diff --git a/.gitignore b/.gitignore
index fab7372..8da5646 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ Thumbs.db
 
 # qtcreator generated files
 *.pro.user*
+*.user
 
 # xemacs temporary files
 *.flc
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 109c28c..7830e5d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.14)
 
-project(scratchcpp-gui VERSION 1.0.0 LANGUAGES CXX)
+project(scratchcpp-render VERSION 1.0.0 LANGUAGES CXX)
 
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 set(CMAKE_AUTOUIC ON)
@@ -9,27 +9,25 @@ set(CMAKE_AUTORCC ON)
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
-option(SCRATCHCPPGUI_BUILD_UNIT_TESTS "Build unit tests" ON)
+option(SCRATCHCPPRENDER_BUILD_UNIT_TESTS "Build unit tests" ON)
 
 include(build/FindQt.cmake)
 
 qt_standard_project_setup(REQUIRES 6.6)
-add_subdirectory(ScratchCPPGui)
+add_subdirectory(src)
 
-set(QML_IMPORT_PATH "${QML_IMPORT_PATH};${PROJECT_SOURCE_DIR}/src/qml" PARENT_SCOPE)
+target_compile_definitions(scratchcpp-render PRIVATE SCRATCHCPPRENDER_LIBRARY)
 
-target_compile_definitions(libscratchcpp-gui PRIVATE SCRATCHCPPGUI_LIBRARY)
-
-linkQt(libscratchcpp-gui)
+linkQt(scratchcpp-render)
 
 include(build/SetUpLibscratchcpp.cmake)
-target_link_libraries(libscratchcpp-gui PRIVATE scratchcpp)
+target_link_libraries(scratchcpp-render PRIVATE scratchcpp)
 
 add_subdirectory(thirdparty/libqnanopainter)
-target_include_directories(libscratchcpp-gui PRIVATE thirdparty/libqnanopainter)
-target_link_libraries(libscratchcpp-gui PRIVATE qnanopainter)
+target_include_directories(scratchcpp-render PRIVATE thirdparty/libqnanopainter)
+target_link_libraries(scratchcpp-render PRIVATE qnanopainter)
 
-if (SCRATCHCPPGUI_BUILD_UNIT_TESTS)
+if (SCRATCHCPPRENDER_BUILD_UNIT_TESTS)
     enable_testing()
     add_subdirectory(test)
 endif()
diff --git a/README.md b/README.md
index e370e1f..1c5522e 100644
--- a/README.md
+++ b/README.md
@@ -20,21 +20,21 @@
 <!-- PROJECT LOGO -->
 <br />
 <div align="center">
-  <!-- TODO: Add logo: <a href="https://github.com/scratchcpp/scratchcpp-gui">
+  <!-- TODO: Add logo: <a href="https://github.com/scratchcpp/scratchcpp-render">
     <img src="images/logo.png" alt="Logo" width="80" height="80">
   </a> -->
 
-<h3 align="center">ScratchCPP GUI</h3>
+<h3 align="center">ScratchCPP Render</h3>
 
   <p align="center">
     A Qt-based library which uses libscratchcpp and QNanoPainter to render running Scratch projects.
     <br />
-    <!-- TODO: Add link to documentation: <a href="https://github.com/scratchcpp/scratchcpp-gui"><strong>Explore the docs »</strong></a>
+    <!-- TODO: Add link to documentation: <a href="https://github.com/scratchcpp/scratchcpp-render"><strong>Explore the docs »</strong></a>
     <br /> -->
     <br />
-    <a href="https://github.com/scratchcpp/scratchcpp-gui/issues">Report Bug</a>
+    <a href="https://github.com/scratchcpp/scratchcpp-render/issues">Report Bug</a>
     ·
-    <a href="https://github.com/scratchcpp/scratchcpp-gui/issues">Request Feature</a>
+    <a href="https://github.com/scratchcpp/scratchcpp-render/issues">Request Feature</a>
   </p>
 </div>
 
@@ -62,7 +62,7 @@
 <!-- ABOUT THE PROJECT -->
 ## About The Project
 
-**scratchcpp-gui** is a library which provides a set of Qt Quick components that can be used to run Scratch projects.
+**scratchcpp-render** is a library which provides a set of Qt Quick components that can be used to run and render Scratch projects.
 
 It uses **QNanoPainter** on top of Qt for hardware accelerated rendering
 and **libscratchcpp** to read, run and maintain the state of Scratch projects.
@@ -88,10 +88,10 @@ We're working on the documentation, it'll be available soon.
 
 <!-- USAGE EXAMPLES -->
 ## Usage
-The most common way to get started is to use the `ProjectPlayer` component.
+Get started by using the `ProjectPlayer` component.
 ```qml
 import QtQuick
-import ScratchCPPGui
+import ScratchCPP.Render
 
 Window {
     width: 480
@@ -114,6 +114,13 @@ Button {
 }
 ```
 
+Please note that the ScratchCPP renderer only works with the basic scene graph render loop.
+Qt 6 uses the threaded render loop by default, so you'll have to disable it by calling this
+before constructing your application object:
+```cpp
+qputenv("QSG_RENDER_LOOP", "basic");
+```
+
 <p align="right">(<a href="#readme-top">back to top</a>)</p>
 
 
@@ -128,19 +135,20 @@ Button {
 - [x] Loading projects from URL
 - [x] SVG rendering
 - [x] Mouse position
-- [ ] Key press events
+- [x] Key press events
 - [x] Mouse press events
-- [ ] Sprite click events
-- [ ] Clones
-- [ ] Sprite dragging
+- [x] Sprite click events
+- [x] Clones
+- [x] Sprite dragging
+- [ ] Touching sprite block
 - [ ] Touching color blocks
 - [ ] Pen blocks
-- [ ] Monitors
+- [x] Monitors
 - [ ] Graphics effects (maybe using shaders)
 - [ ] Speech and thought bubbles
 - [ ] Question text box ("ask and wait" block)
 
-See the [open issues](https://github.com/scratchcpp/scratchcpp-gui/issues) for a full list of proposed features (and known issues).
+See the [open issues](https://github.com/scratchcpp/scratchcpp-render/issues) for a full list of proposed features (and known issues).
 
 <p align="right">(<a href="#readme-top">back to top</a>)</p>
 
@@ -176,13 +184,13 @@ Distributed under the GNU Lesser General Public License. See [LICENSE](LICENSE)
 
 <!-- MARKDOWN LINKS & IMAGES -->
 <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
-[contributors-shield]: https://img.shields.io/github/contributors/scratchcpp/scratchcpp-gui.svg?style=for-the-badge
-[contributors-url]: https://github.com/scratchcpp/scratchcpp-gui/graphs/contributors
-[forks-shield]: https://img.shields.io/github/forks/scratchcpp/scratchcpp-gui.svg?style=for-the-badge
-[forks-url]: https://github.com/scratchcpp/scratchcpp-gui/network/members
-[stars-shield]: https://img.shields.io/github/stars/scratchcpp/scratchcpp-gui.svg?style=for-the-badge
-[stars-url]: https://github.com/scratchcpp/scratchcpp-gui/stargazers
-[issues-shield]: https://img.shields.io/github/issues/scratchcpp/scratchcpp-gui.svg?style=for-the-badge
-[issues-url]: https://github.com/scratchcpp/scratchcpp-gui/issues
-[license-shield]: https://img.shields.io/github/license/scratchcpp/scratchcpp-gui.svg?style=for-the-badge
-[license-url]: https://github.com/scratchcpp/scratchcpp-gui/blob/master/LICENSE
+[contributors-shield]: https://img.shields.io/github/contributors/scratchcpp/scratchcpp-render.svg?style=for-the-badge
+[contributors-url]: https://github.com/scratchcpp/scratchcpp-render/graphs/contributors
+[forks-shield]: https://img.shields.io/github/forks/scratchcpp/scratchcpp-render.svg?style=for-the-badge
+[forks-url]: https://github.com/scratchcpp/scratchcpp-render/network/members
+[stars-shield]: https://img.shields.io/github/stars/scratchcpp/scratchcpp-render.svg?style=for-the-badge
+[stars-url]: https://github.com/scratchcpp/scratchcpp-render/stargazers
+[issues-shield]: https://img.shields.io/github/issues/scratchcpp/scratchcpp-render.svg?style=for-the-badge
+[issues-url]: https://github.com/scratchcpp/scratchcpp-render/issues
+[license-shield]: https://img.shields.io/github/license/scratchcpp/scratchcpp-render.svg?style=for-the-badge
+[license-url]: https://github.com/scratchcpp/scratchcpp-render/blob/master/LICENSE
diff --git a/ScratchCPPGui/CMakeLists.txt b/ScratchCPPGui/CMakeLists.txt
deleted file mode 100644
index b75c67f..0000000
--- a/ScratchCPPGui/CMakeLists.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-qt_add_library(libscratchcpp-gui STATIC)
-
-set_target_properties(libscratchcpp-gui PROPERTIES AUTOMOC ON)
-
-qt_add_qml_module(libscratchcpp-gui
-    URI ScratchCPPGui
-    VERSION 1.0
-    QML_FILES
-        ProjectPlayer.qml
-    SOURCES
-        global.h
-	projectloader.cpp
-	projectloader.h
-        projectscene.cpp
-        projectscene.h
-	stagemodel.cpp
-	stagemodel.h
-	spritemodel.cpp
-	spritemodel.h
-	irenderedtarget.h
-        renderedtarget.cpp
-        renderedtarget.h
-	targetpainter.cpp
-	targetpainter.h
-	keyeventhandler.cpp
-	keyeventhandler.h
-)
diff --git a/ScratchCPPGui/ProjectPlayer.qml b/ScratchCPPGui/ProjectPlayer.qml
deleted file mode 100644
index d304b36..0000000
--- a/ScratchCPPGui/ProjectPlayer.qml
+++ /dev/null
@@ -1,122 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-import QtQuick
-import QtQuick.Layouts
-import QtQuick.Controls
-import ScratchCPPGui
-
-ProjectScene {
-    property string fileName
-    property alias fps: loader.fps
-    property alias turboMode: loader.turboMode
-	property alias cloneLimit: loader.cloneLimit
-	property alias spriteFencing: loader.spriteFencing
-    property bool showLoadingProgress: true
-    readonly property bool loading: priv.loading
-    readonly property int downloadedAssets: loader.downloadedAssets
-    readonly property int assetCount: loader.assetCount
-    signal loaded()
-    signal failedToLoad()
-
-    id: root
-	clip: true
-    engine: loader.engine
-    onFileNameChanged: priv.loading = true;
-
-    QtObject {
-        id: priv
-        property bool loading: false
-    }
-
-    ProjectLoader {
-        id: loader
-        fileName: root.fileName
-		stageWidth: parent.width
-		stageHeight: parent.height
-        onLoadingFinished: {
-            priv.loading = false;
-
-            if(loadStatus)
-                loaded();
-            else
-                failedToLoad();
-        }
-    }
-
-    RenderedTarget {
-        id: stageTarget
-        engine: loader.engine
-        stageModel: loader.stage
-        onStageModelChanged: stageModel.renderedTarget = this
-    }
-
-    function start() {
-        loader.start();
-    }
-
-    function stop() {
-        loader.stop();
-    }
-
-    Repeater {
-        id: sprites
-        model: loader.sprites
-
-        RenderedTarget {
-            id: target
-            engine: loader.engine
-            spriteModel: modelData
-            transform: Scale { xScale: mirrorHorizontally ? -1 : 1 }
-            Component.onCompleted: modelData.renderedTarget = this
-        }
-    }
-
-    Loader {
-        anchors.fill: parent
-        active: showLoadingProgress && loading
-
-        sourceComponent: ColumnLayout {
-            anchors.fill: parent
-
-            Item { Layout.fillHeight: true }
-
-            BusyIndicator {
-                Layout.fillWidth: true
-                Layout.maximumWidth: 100
-                Layout.alignment: Qt.AlignHCenter
-                running: true
-            }
-
-            Label {
-                Layout.alignment: Qt.AlignHCenter
-                font.bold: true
-                font.pointSize: 12
-                text: {
-                    if(loading)
-                        return assetCount == downloadedAssets ? qsTr("Loading project...") : qsTr("Downloading assets... (%1 of %2)").arg(downloadedAssets).arg(assetCount);
-                    else
-                        return "";
-                }
-            }
-
-            ProgressBar {
-                Layout.fillWidth: true
-                from: 0
-                to: assetCount
-                value: downloadedAssets
-                indeterminate: assetCount == downloadedAssets
-            }
-
-            Item { Layout.fillHeight: true }
-        }
-    }
-
-    MouseArea {
-        id: mouseArea
-        anchors.fill: parent
-        hoverEnabled: true
-        onPositionChanged: root.handleMouseMove(mouseX, mouseY)
-        onPressed: root.handleMousePress()
-        onReleased: root.handleMouseRelease()
-    }
-}
diff --git a/ScratchCPPGui/renderedtarget.cpp b/ScratchCPPGui/renderedtarget.cpp
deleted file mode 100644
index 308b807..0000000
--- a/ScratchCPPGui/renderedtarget.cpp
+++ /dev/null
@@ -1,309 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-#include <scratchcpp/iengine.h>
-#include <scratchcpp/costume.h>
-#include <QtSvg/QSvgRenderer>
-#include <qnanopainter.h>
-
-#include "renderedtarget.h"
-#include "targetpainter.h"
-#include "stagemodel.h"
-#include "spritemodel.h"
-
-using namespace scratchcppgui;
-using namespace libscratchcpp;
-
-RenderedTarget::RenderedTarget(QNanoQuickItem *parent) :
-    IRenderedTarget(parent)
-{
-}
-
-void RenderedTarget::loadProperties()
-{
-    Q_ASSERT(!(m_spriteModel && m_stageModel));
-    if (!m_engine || !m_costume)
-        return;
-
-    if (m_spriteModel) {
-        mutex.lock();
-        Sprite *sprite = m_spriteModel->sprite();
-
-        // Visibility
-        m_visible = sprite->visible();
-
-        if (m_visible) {
-            // Direction
-            switch (sprite->rotationStyle()) {
-                case Sprite::RotationStyle::AllAround:
-                    m_rotation = sprite->direction() - 90;
-                    m_newMirrorHorizontally = false;
-
-                    break;
-
-                case Sprite::RotationStyle::LeftRight: {
-                    m_rotation = 0;
-                    m_newMirrorHorizontally = (sprite->direction() < 0);
-
-                    break;
-                }
-
-                case Sprite::RotationStyle::DoNotRotate:
-                    m_rotation = 0;
-                    m_newMirrorHorizontally = false;
-                    break;
-            }
-
-            // Coordinates
-            double size = sprite->size() / 100;
-            m_x = static_cast<double>(m_engine->stageWidth()) / 2 + sprite->x() - m_costume->rotationCenterX() * size / m_costume->bitmapResolution() * (m_newMirrorHorizontally ? -1 : 1);
-            m_y = static_cast<double>(m_engine->stageHeight()) / 2 - sprite->y() - m_costume->rotationCenterY() * size / m_costume->bitmapResolution();
-            m_originX = m_costume->rotationCenterX() * size / m_costume->bitmapResolution();
-            m_originY = m_costume->rotationCenterY() * size / m_costume->bitmapResolution();
-
-            // Layer
-            m_z = sprite->layerOrder();
-        }
-
-        mutex.unlock();
-    } else if (m_stageModel) {
-        m_x = static_cast<double>(m_engine->stageWidth()) / 2 - m_costume->rotationCenterX() / m_costume->bitmapResolution();
-        m_y = static_cast<double>(m_engine->stageHeight()) / 2 - m_costume->rotationCenterY() / m_costume->bitmapResolution();
-        m_originX = m_costume->rotationCenterX() / m_costume->bitmapResolution();
-        m_originY = m_costume->rotationCenterY() / m_costume->bitmapResolution();
-    }
-}
-
-void RenderedTarget::loadCostume(Costume *costume)
-{
-    if (!costume)
-        return;
-
-    m_costumeMutex.lock();
-    m_imageChanged = true;
-
-    if (costume->dataFormat() == "svg") {
-        if (costume != m_costume)
-            m_svgRenderer.load(QByteArray::fromRawData(static_cast<const char *>(costume->data()), costume->dataSize()));
-    }
-
-    m_costume = costume;
-    m_costumeMutex.unlock();
-}
-
-void RenderedTarget::updateProperties()
-{
-    mutex.lock();
-    setVisible(m_visible);
-
-    if (m_visible) {
-        if (m_imageChanged) {
-            doLoadCostume();
-            update();
-            m_imageChanged = false;
-        }
-
-        setX(m_x);
-        setY(m_y);
-        setZ(m_z);
-        setRotation(m_rotation);
-        setTransformOriginPoint(QPointF(m_originX, m_originY));
-
-        if (m_newMirrorHorizontally != m_mirrorHorizontally) {
-            m_mirrorHorizontally = m_newMirrorHorizontally;
-            emit mirrorHorizontallyChanged();
-        }
-    }
-
-    mutex.unlock();
-}
-
-IEngine *RenderedTarget::engine() const
-{
-    return m_engine;
-}
-
-void RenderedTarget::setEngine(IEngine *newEngine)
-{
-    if (m_engine == newEngine)
-        return;
-
-    m_engine = newEngine;
-    emit engineChanged();
-}
-
-StageModel *RenderedTarget::stageModel() const
-{
-    return m_stageModel;
-}
-
-void RenderedTarget::setStageModel(StageModel *newStageModel)
-{
-    if (m_stageModel == newStageModel)
-        return;
-
-    m_stageModel = newStageModel;
-    emit stageModelChanged();
-}
-
-SpriteModel *RenderedTarget::spriteModel() const
-{
-    return m_spriteModel;
-}
-
-void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel)
-{
-    if (m_spriteModel == newSpriteModel)
-        return;
-
-    m_spriteModel = newSpriteModel;
-    emit spriteModelChanged();
-}
-
-Target *RenderedTarget::scratchTarget() const
-{
-    if (m_spriteModel)
-        return m_spriteModel->sprite();
-    else if (m_stageModel)
-        return m_stageModel->stage();
-    else
-        return nullptr;
-}
-
-qreal RenderedTarget::width() const
-{
-    return QNanoQuickItem::width();
-}
-
-void RenderedTarget::setWidth(qreal width)
-{
-    QNanoQuickItem::setWidth(width);
-}
-
-qreal RenderedTarget::height() const
-{
-    return QNanoQuickItem::height();
-}
-
-void RenderedTarget::setHeight(qreal height)
-{
-    QNanoQuickItem::setHeight(height);
-}
-
-QNanoQuickItemPainter *RenderedTarget::createItemPainter() const
-{
-    return new TargetPainter();
-}
-
-void RenderedTarget::doLoadCostume()
-{
-    m_costumeMutex.lock();
-
-    if (!m_costume) {
-        m_costumeMutex.unlock();
-        return;
-    }
-
-    Target *target = scratchTarget();
-
-    if (m_costume->dataFormat() == "svg") {
-        QRectF rect = m_svgRenderer.viewBoxF();
-        calculateSize(target, rect.width(), rect.height());
-    } else {
-        m_bitmapBuffer.open(QBuffer::WriteOnly);
-        m_bitmapBuffer.write(static_cast<const char *>(m_costume->data()), m_costume->dataSize());
-        m_bitmapBuffer.close();
-        m_bitmapUniqueKey = QString::fromStdString(m_costume->id());
-
-        QImageReader reader(&m_bitmapBuffer);
-        QSize size = reader.size();
-        calculateSize(target, size.width(), size.height());
-        m_bitmapBuffer.close();
-    }
-
-    m_costumeMutex.unlock();
-}
-
-void RenderedTarget::paintSvg(QNanoPainter *painter)
-{
-    Q_ASSERT(painter);
-    QOpenGLContext *context = QOpenGLContext::currentContext();
-    Q_ASSERT(context);
-
-    if (!context)
-        return;
-
-    QOffscreenSurface surface;
-    surface.setFormat(context->format());
-    surface.create();
-    Q_ASSERT(surface.isValid());
-
-    QSurface *oldSurface = context->surface();
-    context->makeCurrent(&surface);
-
-    const QRectF drawRect(0, 0, width(), height());
-    const QSize drawRectSize = drawRect.size().toSize();
-
-    /*QOpenGLFramebufferObjectFormat fboFormat;
-    fboFormat.setSamples(16);
-    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);*/
-
-    QOpenGLPaintDevice device(drawRectSize);
-    QPainter qPainter;
-    qPainter.begin(&device);
-    qPainter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
-    m_svgRenderer.render(&qPainter, drawRect);
-    qPainter.end();
-
-    context->doneCurrent();
-    context->makeCurrent(oldSurface);
-}
-
-void RenderedTarget::calculateSize(Target *target, double costumeWidth, double costumeHeight)
-{
-    if (m_costume) {
-        double bitmapRes = m_costume->bitmapResolution();
-        Sprite *sprite = dynamic_cast<Sprite *>(target);
-
-        if (sprite) {
-            double size = sprite->size();
-            setWidth(costumeWidth * size / 100 / bitmapRes);
-            setHeight(costumeHeight * size / 100 / bitmapRes);
-        } else {
-            setWidth(costumeWidth / bitmapRes);
-            setHeight(costumeHeight / bitmapRes);
-        }
-    }
-}
-
-QBuffer *RenderedTarget::bitmapBuffer()
-{
-    return &m_bitmapBuffer;
-}
-
-const QString &RenderedTarget::bitmapUniqueKey() const
-{
-    return m_bitmapUniqueKey;
-}
-
-void RenderedTarget::lockCostume()
-{
-    m_costumeMutex.lock();
-}
-
-void RenderedTarget::unlockCostume()
-{
-    m_costumeMutex.unlock();
-}
-
-bool RenderedTarget::mirrorHorizontally() const
-{
-    return m_mirrorHorizontally;
-}
-
-bool RenderedTarget::isSvg() const
-{
-    if (!m_costume)
-        return false;
-
-    return (m_costume->dataFormat() == "svg");
-}
diff --git a/ScratchCPPGui/targetpainter.cpp b/ScratchCPPGui/targetpainter.cpp
deleted file mode 100644
index 21595bf..0000000
--- a/ScratchCPPGui/targetpainter.cpp
+++ /dev/null
@@ -1,40 +0,0 @@
-// SPDX-License-Identifier: LGPL-3.0-or-later
-
-#include <scratchcpp/target.h>
-#include <scratchcpp/costume.h>
-
-#include "targetpainter.h"
-#include "irenderedtarget.h"
-#include "spritemodel.h"
-
-using namespace scratchcppgui;
-
-TargetPainter::TargetPainter()
-{
-}
-
-TargetPainter::~TargetPainter()
-{
-}
-
-void TargetPainter::paint(QNanoPainter *painter)
-{
-    m_target->lockCostume();
-    double width = m_target->width();
-    double height = m_target->height();
-
-    if (m_target->isSvg())
-        m_target->paintSvg(painter);
-    else {
-        QNanoImage image = QNanoImage::fromCache(painter, m_target->bitmapBuffer(), m_target->bitmapUniqueKey());
-        painter->drawImage(image, 0, 0, width, height);
-    }
-
-    m_target->unlockCostume();
-}
-
-void TargetPainter::synchronize(QNanoQuickItem *item)
-{
-    m_target = dynamic_cast<IRenderedTarget *>(item);
-    Q_ASSERT(m_target);
-}
diff --git a/build/FindQt.cmake b/build/FindQt.cmake
index 4497e7b..289fe8a 100644
--- a/build/FindQt.cmake
+++ b/build/FindQt.cmake
@@ -1,7 +1,7 @@
 find_package(Qt6 6.6 COMPONENTS Gui Qml Quick Svg REQUIRED)
 set(QT_LIBS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::Svg)
 
-if (SCRATCHCPPGUI_BUILD_UNIT_TESTS)
+if (SCRATCHCPPRENDER_BUILD_UNIT_TESTS)
     find_package(Qt6 6.6 COMPONENTS Test REQUIRED)
 endif()
 
diff --git a/build/SetUpLibscratchcpp.cmake b/build/SetUpLibscratchcpp.cmake
index 3447ad4..4904313 100644
--- a/build/SetUpLibscratchcpp.cmake
+++ b/build/SetUpLibscratchcpp.cmake
@@ -1,3 +1,3 @@
 set(LIBSCRATCHCPP_BUILD_UNIT_TESTS OFF)
 add_subdirectory(${PROJECT_SOURCE_DIR}/libscratchcpp libscratchcpp)
-target_include_directories(libscratchcpp-gui PUBLIC ${PROJECT_SOURCE_DIR}/libscratchcpp/include)
+target_include_directories(scratchcpp-render PUBLIC ${PROJECT_SOURCE_DIR}/libscratchcpp/include)
diff --git a/libscratchcpp b/libscratchcpp
index 0acdf37..d7e05e2 160000
--- a/libscratchcpp
+++ b/libscratchcpp
@@ -1 +1 @@
-Subproject commit 0acdf371cfed90cce4e35b6bbf59957672dfe1ae
+Subproject commit d7e05e2c01f15ecfad78e8a427b7cea9ce4253ec
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..5cd33b5
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,48 @@
+qt_add_library(scratchcpp-render STATIC)
+
+set_target_properties(scratchcpp-render PROPERTIES AUTOMOC ON)
+
+qt_add_qml_module(scratchcpp-render
+    URI ScratchCPP.Render
+    VERSION 1.0
+    OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ScratchCPP/Render
+    QML_FILES
+        ProjectPlayer.qml
+    RESOURCES
+        internal/ValueMonitor.qml
+	internal/MonitorSlider.qml
+	internal/ListMonitor.qml
+    SOURCES
+        global.h
+	projectloader.cpp
+	projectloader.h
+        projectscene.cpp
+        projectscene.h
+	stagemodel.cpp
+	stagemodel.h
+	spritemodel.cpp
+	spritemodel.h
+	monitormodel.cpp
+	monitormodel.h
+	valuemonitormodel.cpp
+	valuemonitormodel.h
+	listmonitormodel.cpp
+	listmonitormodel.h
+	listmonitorlistmodel.cpp
+	listmonitorlistmodel.h
+	irenderedtarget.h
+        renderedtarget.cpp
+        renderedtarget.h
+	targetpainter.cpp
+	targetpainter.h
+	scenemousearea.cpp
+	scenemousearea.h
+	mouseeventhandler.cpp
+	mouseeventhandler.h
+	keyeventhandler.cpp
+	keyeventhandler.h
+)
+
+list(APPEND QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
+list(REMOVE_DUPLICATES QML_IMPORT_PATH)
+set(QML_IMPORT_PATH ${QML_IMPORT_PATH} CACHE STRING "" FORCE)
diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml
new file mode 100644
index 0000000..908dbf6
--- /dev/null
+++ b/src/ProjectPlayer.qml
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import ScratchCPP.Render
+
+import "internal"
+
+ProjectScene {
+    property string fileName
+    property int stageWidth: 480
+    property int stageHeight: 360
+    property alias fps: loader.fps
+    property alias turboMode: loader.turboMode
+	property alias cloneLimit: loader.cloneLimit
+	property alias spriteFencing: loader.spriteFencing
+    property bool showLoadingProgress: true
+    readonly property bool loading: priv.loading
+    readonly property int downloadedAssets: loader.downloadedAssets
+    readonly property int assetCount: loader.assetCount
+    readonly property Rectangle stageRect: contentRect
+    signal loaded()
+    signal failedToLoad()
+
+    id: root
+    engine: loader.engine
+    stageScale: (stageWidth == 0 || stageHeight == 0) ? 1 : Math.min(width / stageWidth, height / stageHeight)
+    onFileNameChanged: priv.loading = true;
+
+    QtObject {
+        id: priv
+        property bool loading: false
+    }
+
+    ProjectLoader {
+        id: loader
+        fileName: root.fileName
+        stageWidth: root.stageWidth
+        stageHeight: root.stageHeight
+
+        onLoadingFinished: {
+            priv.loading = false;
+
+            if(loadStatus)
+                loaded();
+            else
+                failedToLoad();
+        }
+
+        onStageChanged: stage.loadCostume();
+
+        onCloneCreated: (cloneModel)=> clones.model.append({"spriteModel": cloneModel})
+
+        onCloneDeleted: (cloneModel)=> {
+            // TODO: Removing the clone from C++ would probably be faster
+            let i;
+
+            for(i = 0; i < clones.model.count; i++) {
+                if(clones.model.get(i).spriteModel === cloneModel)
+                    break;
+            }
+
+            if(i === clones.model.count)
+                console.error("error: deleted clone doesn't exist");
+            else
+                clones.model.remove(i);
+        }
+
+        onMonitorAdded: (monitorModel)=> monitors.model.append({"monitorModel": monitorModel})
+
+        onMonitorRemoved: (monitorModel)=> {
+            // TODO: Removing the monitor from C++ would probably be faster
+            let i;
+
+            for(i = 0; i < monitors.model.count; i++) {
+                if(monitors.model.get(i).monitorModel === monitorModel)
+                    break;
+            }
+
+            if(i !== monitors.model.count)
+                monitors.model.remove(i);
+        }
+    }
+
+    function start() {
+        loader.start();
+    }
+
+    function stop() {
+        loader.stop();
+    }
+
+    Rectangle {
+        id: contentRect
+        anchors.horizontalCenter: parent.horizontalCenter
+        anchors.top: parent.top
+        width: stageWidth * stageScale
+        height: stageHeight * stageScale
+        color: priv.loading ? "transparent" : "white"
+        clip: true
+
+        RenderedTarget {
+            id: stageTarget
+            engine: loader.engine
+            stageModel: loader.stage
+            mouseArea: sceneMouseArea
+            stageScale: root.stageScale
+            onStageModelChanged: stageModel.renderedTarget = this
+        }
+
+        Component {
+            id: renderedSprite
+
+            RenderedTarget {
+                id: target
+                mouseArea: sceneMouseArea
+                stageScale: root.stageScale
+                transform: Scale { xScale: mirrorHorizontally ? -1 : 1 }
+                Component.onCompleted: {
+                    engine = loader.engine;
+                    spriteModel = modelData;
+                    spriteModel.renderedTarget = this;
+                }
+            }
+        }
+
+        Repeater {
+            id: sprites
+            model: loader.sprites
+            delegate: renderedSprite
+        }
+
+        Repeater {
+            id: clones
+            model: ListModel {}
+            delegate: renderedSprite
+        }
+
+        SceneMouseArea {
+            id: sceneMouseArea
+            anchors.fill: parent
+            stage: stageTarget
+            projectLoader: loader
+            onMouseMoved: (x, y)=> root.handleMouseMove(x, y)
+            onMousePressed: root.handleMousePress()
+            onMouseReleased: root.handleMouseRelease()
+        }
+
+        Component {
+            id: renderedValueMonitor
+
+            ValueMonitor {
+                model: parent.model
+                scale: root.stageScale
+                transformOrigin: Item.TopLeft
+                x: model.x * scale
+                y: model.y * scale
+            }
+        }
+
+        Component {
+            id: renderedListMonitor
+
+            ListMonitor {
+                model: parent.model
+                scale: root.stageScale
+                transformOrigin: Item.TopLeft
+                x: model.x * scale
+                y: model.y * scale
+            }
+        }
+
+        Component {
+            id: renderedMonitor
+
+            Loader {
+                readonly property MonitorModel model: monitorModel
+                sourceComponent: monitorModel ? (monitorModel.type === MonitorModel.Value ? renderedValueMonitor : renderedListMonitor) : null
+                active: sourceComponent != null
+                z: loader.sprites.length + loader.clones.length + 1 // above all sprites
+            }
+        }
+
+        Repeater {
+            id: monitors
+            model: ListModel {}
+            delegate: renderedMonitor
+        }
+
+        Loader {
+            anchors.fill: parent
+            active: showLoadingProgress && loading
+
+            sourceComponent: ColumnLayout {
+                anchors.fill: parent
+
+                Item { Layout.fillHeight: true }
+
+                BusyIndicator {
+                    Layout.fillWidth: true
+                    Layout.maximumWidth: 100
+                    Layout.alignment: Qt.AlignHCenter
+                    running: true
+                }
+
+                Label {
+                    Layout.alignment: Qt.AlignHCenter
+                    font.bold: true
+                    font.pointSize: 12
+                    text: {
+                        if(loading)
+                            return assetCount == downloadedAssets ? qsTr("Loading project...") : qsTr("Downloading assets... (%1 of %2)").arg(downloadedAssets).arg(assetCount);
+                        else
+                            return "";
+                    }
+                }
+
+                ProgressBar {
+                    Layout.fillWidth: true
+                    from: 0
+                    to: assetCount
+                    value: downloadedAssets
+                    indeterminate: assetCount == downloadedAssets
+                }
+
+                Item { Layout.fillHeight: true }
+            }
+        }
+    }
+}
diff --git a/ScratchCPPGui/global.h b/src/global.h
similarity index 100%
rename from ScratchCPPGui/global.h
rename to src/global.h
diff --git a/src/internal/ListMonitor.qml b/src/internal/ListMonitor.qml
new file mode 100644
index 0000000..36e02d4
--- /dev/null
+++ b/src/internal/ListMonitor.qml
@@ -0,0 +1,201 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import ScratchCPP.Render
+
+// NOTE: All the values here make list monitors look
+// like on Scratch, so be careful when doing any changes.
+Rectangle {
+    id: root
+    property ListMonitorModel model: null
+
+    width: model ? (model.width > 0 ? model.width : priv.defaultWidth) : priv.defaultWidth
+    height: model ? (model.height > 0 ? model.height : priv.defaultHeight) : priv.defaultHeight
+    color: Qt.rgba(0.9, 0.94, 1, 1)
+    border.color: Qt.rgba(0.765, 0.8, 0.85, 1)
+    radius: 5
+    visible: model ? model.visible : true
+
+    QtObject {
+        id: priv
+        readonly property int defaultWidth: 102
+        readonly property int defaultHeight: 203
+        readonly property color textColor: Qt.rgba(0.34, 0.37, 0.46, 1)
+    }
+
+    Text {
+        id: header
+        anchors.left: parent.left
+        anchors.top: parent.top
+        width: parent.width
+        color: priv.textColor
+        text: model ? model.name : ""
+        textFormat: Text.PlainText
+        font.pointSize: 9
+        font.bold: true
+        font.family: "Helvetica"
+        horizontalAlignment: Qt.AlignHCenter
+        wrapMode: Text.WordWrap
+        clip: true
+        padding: 3
+
+        Rectangle {
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.top: parent.top
+            anchors.leftMargin: 1
+            anchors.topMargin: 1
+            anchors.rightMargin: 1
+            height: root.height
+            color: "white"
+            radius: root.radius
+            z: -1
+        }
+    }
+
+    Text {
+        id: emptyText
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.top: header.bottom
+        visible: listView.count <= 0
+        color: priv.textColor
+        text: qsTr("(empty)")
+        textFormat: Text.PlainText
+        font.pointSize: 9
+        font.family: "Helvetica"
+        horizontalAlignment: Qt.AlignHCenter
+        wrapMode: Text.WordWrap
+        clip: true
+        padding: 3
+    }
+
+    ListView {
+        property real oldContentY
+        readonly property int scrollBarWidth: 15
+
+        id: listView
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.top: header.bottom
+        anchors.bottom: footer.top
+        anchors.topMargin: 1
+        clip: true
+        model: root.model ? root.model.listModel : null
+        boundsBehavior: Flickable.StopAtBounds
+
+        ScrollBar.vertical: ScrollBar {
+            id: scrollBar
+            anchors.right: listView.right
+            anchors.rightMargin: 2
+            width: 13
+            visible: scrollBar.size < 1
+            policy: ScrollBar.AlwaysOn
+
+            contentItem: Rectangle {
+                color: scrollBar.pressed ? Qt.rgba(0.47, 0.47, 0.47, 1) : (hoverHandler.hovered ? Qt.rgba(0.66, 0.66, 0.66, 1) : Qt.rgba(0.76, 0.76, 0.76, 1))
+
+                HoverHandler {
+                    id: hoverHandler
+                }
+            }
+
+            background: null // background is a separate component because contentItem width can't be changed
+        }
+
+        Rectangle {
+            // Scroll bar background
+            id: scrollBarBg
+            anchors.right: parent.right
+            anchors.top: parent.top
+            anchors.bottom: parent.bottom
+            anchors.rightMargin: 1
+            visible: scrollBar.visible
+            width: listView.scrollBarWidth
+            color: Qt.rgba(0.95, 0.95, 0.95, 1)
+        }
+
+        delegate: RowLayout {
+            width: scrollBar.visible ? listView.width - listView.scrollBarWidth - 6 : listView.width - 6
+            height: implicitHeight + 2
+            spacing: 6
+
+            Text {
+                color: priv.textColor
+                text: index + 1
+                font.pointSize: 9
+                font.bold: true
+                font.family: "Helvetica"
+                Layout.leftMargin: 6
+            }
+
+            Item {
+                height: 22
+                Layout.fillWidth: true
+
+                TextEdit {
+                    // TextEdit instead of Text for mouse selection
+                    id: itemText
+                    anchors.left: parent.left
+                    anchors.leftMargin: 3
+                    color: "white"
+                    text: value
+                    textFormat: TextEdit.PlainText
+                    font.pointSize: 9
+                    font.family: "Helvetica"
+                    selectByMouse: true
+                    padding: 2
+                    Layout.rightMargin: 6
+                }
+
+                Rectangle {
+                    anchors.fill: parent
+                    color: root.model ? root.model.color : "green"
+                    border.color: color.darker(1.2)
+                    radius: root.radius
+                    z: -1
+                }
+            }
+        }
+    }
+
+    Text {
+        id: footer
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.bottom: parent.bottom
+        color: priv.textColor
+        text: qsTr("length %1").arg(listView.count)
+        textFormat: Text.PlainText
+        font.pointSize: 9
+        font.bold: true
+        font.family: "Helvetica"
+        horizontalAlignment: Qt.AlignHCenter
+        wrapMode: Text.WordWrap
+        clip: true
+        padding: 3
+
+        Rectangle {
+            anchors.fill: parent
+            anchors.leftMargin: 1
+            anchors.bottomMargin: 1
+            anchors.rightMargin: 1
+            anchors.topMargin: -5
+            color: "white"
+            radius: root.radius
+            z: -1
+        }
+    }
+
+    Rectangle {
+        // for header and footer borders
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.top: header.bottom
+        anchors.bottom: footer.top
+        color: "transparent"
+        border.color: root.border.color
+    }
+}
diff --git a/src/internal/MonitorSlider.qml b/src/internal/MonitorSlider.qml
new file mode 100644
index 0000000..d9a02f8
--- /dev/null
+++ b/src/internal/MonitorSlider.qml
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Controls.Material
+
+// NOTE: All the values here make monitor sliders look exactly
+// like on Scratch, so be careful when doing any changes.
+Slider {
+    id: control
+    property bool discrete: false
+
+    QtObject {
+        id: priv
+        readonly property color bgColor: Qt.rgba(0.94, 0.94, 0.94, 1)
+        readonly property color currentBgColor: control.pressed ? Qt.rgba(0.96, 0.96, 0.96, 1) : (hoverHandler.hovered ? Qt.darker(bgColor, 1.05) : bgColor)
+
+        readonly property color bgBorderColor: Qt.rgba(0.7, 0.7, 0.7, 1)
+        readonly property color currentBgBorderColor: control.pressed ? Qt.rgba(0.77, 0.77, 0.77, 1) : (hoverHandler.hovered ? Qt.darker(bgBorderColor, 1.1) : bgBorderColor)
+
+        readonly property color sliderColor: Qt.rgba(0, 0.46, 1, 1)
+        readonly property color currentSliderColor: control.pressed ? Qt.rgba(0.22, 0.58, 1, 1) : (hoverHandler.hovered ? Qt.darker(sliderColor, 1.28) : sliderColor)
+
+        readonly property color positionBorderColor: Qt.rgba(0.21, 0.46, 0.75, 1)
+        readonly property color currentPositionBorderColor: control.pressed ? Qt.rgba(0.37, 0.56, 0.79, 1) : (hoverHandler.hovered ? Qt.darker(positionBorderColor, 1.28) : positionBorderColor)
+    }
+
+    stepSize: discrete ? 1 : 0.1
+    snapMode: Slider.SnapAlways
+    implicitWidth: 119
+    implicitHeight: 16
+    leftPadding: -4
+    rightPadding: -4
+    topPadding: -1
+    bottomPadding: 0
+    activeFocusOnTab: false
+    hoverEnabled: true
+
+    background: Rectangle {
+        x: control.leftPadding
+        y: control.topPadding + control.availableHeight / 2 - height / 2
+        implicitWidth: 200
+        implicitHeight: 7.5
+        width: control.availableWidth
+        height: implicitHeight
+        radius: 6
+        color: priv.currentBgColor
+        border.color: priv.currentBgBorderColor
+
+        Rectangle {
+            width: control.visualPosition * (parent.width - handle.width) + handle.width
+            height: parent.height
+            color: priv.currentSliderColor
+            border.color: priv.currentPositionBorderColor
+            radius: 6
+        }
+    }
+
+    handle: Rectangle {
+        x: control.leftPadding + control.visualPosition * (control.availableWidth - width)
+        y: control.topPadding + control.availableHeight / 2 - height / 2
+        implicitWidth: 15
+        implicitHeight: 15
+        radius: 7
+        color: control.pressed ? priv.currentSliderColor : (handleHoverHandler.hovered ? priv.currentSliderColor : priv.sliderColor)
+
+        HoverHandler {
+            id: handleHoverHandler
+        }
+    }
+
+    HoverHandler {
+        id: hoverHandler
+    }
+}
diff --git a/src/internal/ValueMonitor.qml b/src/internal/ValueMonitor.qml
new file mode 100644
index 0000000..74dd34f
--- /dev/null
+++ b/src/internal/ValueMonitor.qml
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+import QtQuick
+import QtQuick.Layouts
+import ScratchCPP.Render
+
+// NOTE: All the values here make monitors look exactly
+// like on Scratch, so be careful when doing any changes.
+Rectangle {
+    property ValueMonitorModel model: null
+
+    color: model ? (model.mode === ValueMonitorModel.Large ? model.color : priv.bgColor) : priv.bgColor
+    border.color: Qt.rgba(0.765, 0.8, 0.85, 1)
+    radius: 5
+    width: layout.implicitWidth + priv.horizontalMargins * 2
+    height: layout.implicitHeight + priv.verticalMargins * 2
+    visible: model ? model.visible : true
+
+    QtObject {
+        id: priv
+        readonly property int horizontalMargins: 9
+        readonly property double verticalMargins: 2
+        readonly property color bgColor: Qt.rgba(0.9, 0.94, 1, 1)
+    }
+
+    ColumnLayout {
+        id: layout
+        anchors.leftMargin: priv.horizontalMargins
+        anchors.rightMargin: priv.horizontalMargins
+        anchors.topMargin: priv.verticalMargins
+        anchors.bottomMargin: priv.verticalMargins
+        anchors.centerIn: parent
+        spacing: 0
+
+        Loader {
+            active: model ? model.mode !== ValueMonitorModel.Large : true
+            sourceComponent: RowLayout {
+                spacing: 9
+
+                Text {
+                    color: Qt.rgba(0.34, 0.37, 0.46, 1)
+                    text: model ? model.name : ""
+                    textFormat: Text.PlainText
+                    font.pointSize: 9
+                    font.bold: true
+                    font.family: "Helvetica"
+                }
+
+                Text {
+                    id: valueText
+                    color: "white"
+                    text: model ? model.value : ""
+                    textFormat: Text.PlainText
+                    font.pointSize: 9
+                    font.family: "Helvetica"
+                    leftPadding: 2
+                    rightPadding: 2
+                    topPadding: -2
+                    bottomPadding: -1
+                    horizontalAlignment: Qt.AlignHCenter
+                    Layout.minimumWidth: 40
+
+                    Rectangle {
+                        color: model ? model.color : "green"
+                        radius: 5
+                        anchors.fill: parent
+                        anchors.margins: 0
+                        z: -1
+                    }
+                }
+            }
+        }
+
+        Loader {
+            active: model ? model.mode === ValueMonitorModel.Slider : false
+            sourceComponent: MonitorSlider {
+                from: model ? model.sliderMin : 0
+                to: model ? model.sliderMax : 0
+                discrete: model ? model.discrete : true
+                value: model ? model.value : 0
+                Layout.fillWidth: true
+                onMoved: {
+                    if(model)
+                        model.value = value;
+                }
+            }
+        }
+
+        Loader {
+            active: model ? model.mode === ValueMonitorModel.Large : false
+            sourceComponent: Text {
+                color: "white"
+                text: model ? model.value : ""
+                textFormat: Text.PlainText
+                font.pointSize: 12
+                font.family: "Helvetica"
+                leftPadding: 2
+                rightPadding: 2
+                topPadding: -3
+                bottomPadding: 0
+                horizontalAlignment: Qt.AlignHCenter
+                width: Math.max(31, implicitWidth)
+                height: Math.max(14.46, implicitHeight)
+            }
+        }
+    }
+}
diff --git a/ScratchCPPGui/irenderedtarget.h b/src/irenderedtarget.h
similarity index 57%
rename from ScratchCPPGui/irenderedtarget.h
rename to src/irenderedtarget.h
index cbef793..d3295ae 100644
--- a/ScratchCPPGui/irenderedtarget.h
+++ b/src/irenderedtarget.h
@@ -3,24 +3,18 @@
 #pragma once
 
 #include <qnanoquickitem.h>
+#include <scratchcpp/sprite.h>
 
 class QBuffer;
 class QNanoPainter;
+class QOpenGLContext;
 
-namespace libscratchcpp
-{
-
-class Costume;
-class IEngine;
-class Target;
-
-} // namespace libscratchcpp
-
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class StageModel;
 class SpriteModel;
+class SceneMouseArea;
 
 class IRenderedTarget : public QNanoQuickItem
 {
@@ -32,9 +26,19 @@ class IRenderedTarget : public QNanoQuickItem
 
         virtual ~IRenderedTarget() { }
 
-        virtual void loadProperties() = 0;
+        virtual void updateVisibility(bool visible) = 0;
+        virtual void updateX(double x) = 0;
+        virtual void updateY(double y) = 0;
+        virtual void updateSize(double size) = 0;
+        virtual void updateDirection(double direction) = 0;
+        virtual void updateRotationStyle(libscratchcpp::Sprite::RotationStyle style) = 0;
+        virtual void updateLayerOrder(int layerOrder) = 0;
+
         virtual void loadCostume(libscratchcpp::Costume *costume) = 0;
-        virtual void updateProperties() = 0;
+
+        virtual void beforeRedraw() = 0;
+
+        virtual void deinitClone() = 0;
 
         virtual libscratchcpp::IEngine *engine() const = 0;
         virtual void setEngine(libscratchcpp::IEngine *newEngine) = 0;
@@ -47,12 +51,20 @@ class IRenderedTarget : public QNanoQuickItem
 
         virtual libscratchcpp::Target *scratchTarget() const = 0;
 
+        virtual SceneMouseArea *mouseArea() const = 0;
+        virtual void setMouseArea(SceneMouseArea *newMouseArea) = 0;
+
+        virtual double stageScale() const = 0;
+        virtual void setStageScale(double scale) = 0;
+
         virtual qreal width() const = 0;
         virtual void setWidth(qreal width) = 0;
 
         virtual qreal height() const = 0;
         virtual void setHeight(qreal width) = 0;
 
+        virtual QPointF mapFromScene(const QPointF &point) const = 0;
+
         virtual QBuffer *bitmapBuffer() = 0;
         virtual const QString &bitmapUniqueKey() const = 0;
 
@@ -63,6 +75,9 @@ class IRenderedTarget : public QNanoQuickItem
 
         virtual bool isSvg() const = 0;
         virtual void paintSvg(QNanoPainter *painter) = 0;
+
+        virtual void updateHullPoints(QOpenGLFramebufferObject *fbo) = 0;
+        virtual const std::vector<QPointF> &hullPoints() const = 0;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/ScratchCPPGui/keyeventhandler.cpp b/src/keyeventhandler.cpp
similarity index 95%
rename from ScratchCPPGui/keyeventhandler.cpp
rename to src/keyeventhandler.cpp
index 1ff5c04..a77871e 100644
--- a/ScratchCPPGui/keyeventhandler.cpp
+++ b/src/keyeventhandler.cpp
@@ -4,7 +4,7 @@
 
 #include "keyeventhandler.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 
 KeyEventHandler::KeyEventHandler(QObject *parent) :
     QObject(parent)
diff --git a/ScratchCPPGui/keyeventhandler.h b/src/keyeventhandler.h
similarity index 85%
rename from ScratchCPPGui/keyeventhandler.h
rename to src/keyeventhandler.h
index 87f39db..7bd85f5 100644
--- a/ScratchCPPGui/keyeventhandler.h
+++ b/src/keyeventhandler.h
@@ -4,7 +4,7 @@
 
 #include <QObject>
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class KeyEventHandler : public QObject
@@ -13,12 +13,11 @@ class KeyEventHandler : public QObject
     public:
         explicit KeyEventHandler(QObject *parent = nullptr);
 
+        bool eventFilter(QObject *obj, QEvent *event) override;
+
     signals:
         void keyPressed(Qt::Key key, const QString &text);
         void keyReleased(Qt::Key key, const QString &text);
-
-    protected:
-        bool eventFilter(QObject *obj, QEvent *event) override;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/src/listmonitorlistmodel.cpp b/src/listmonitorlistmodel.cpp
new file mode 100644
index 0000000..3cd7fd7
--- /dev/null
+++ b/src/listmonitorlistmodel.cpp
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <scratchcpp/list.h>
+
+#include "listmonitorlistmodel.h"
+
+using namespace scratchcpprender;
+
+ListMonitorListModel::ListMonitorListModel(QObject *parent) :
+    QAbstractListModel(parent)
+{
+}
+
+void ListMonitorListModel::setList(libscratchcpp::List *list)
+{
+    if (!list)
+        return;
+
+    // Initial load
+    if (m_list != list) {
+        beginResetModel();
+        m_list = list;
+        m_oldRowCount = m_list->size();
+        endResetModel();
+        return;
+    }
+
+    // Notify about changed items
+    int count = std::min(m_oldRowCount, static_cast<int>(m_list->size()));
+
+    for (int i = 0; i < count; i++)
+        emit dataChanged(index(i), index(i));
+
+    // Notify about new items (at the end of the list)
+    if (m_list->size() > m_oldRowCount) {
+        beginInsertRows(QModelIndex(), m_oldRowCount, m_list->size() - 1);
+        endInsertRows();
+    } else if (m_list->size() < m_oldRowCount) {
+        // Notify about removed items (at the end of the list)
+        beginRemoveRows(QModelIndex(), m_list->size(), m_oldRowCount - 1);
+        endRemoveRows();
+    }
+
+    m_oldRowCount = m_list->size();
+}
+
+int ListMonitorListModel::rowCount(const QModelIndex &parent) const
+{
+    if (m_list)
+        return m_list->size();
+    else
+        return 0;
+}
+
+QVariant ListMonitorListModel::data(const QModelIndex &index, int role) const
+{
+    if (!m_list || index.row() < 0 || index.row() >= m_list->size())
+        return QVariant();
+
+    return QString::fromStdString((*m_list)[index.row()].toString());
+}
+
+QHash<int, QByteArray> ListMonitorListModel::roleNames() const
+{
+    return { { 0, "value" } };
+}
diff --git a/src/listmonitorlistmodel.h b/src/listmonitorlistmodel.h
new file mode 100644
index 0000000..cb0a8ce
--- /dev/null
+++ b/src/listmonitorlistmodel.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+
+namespace libscratchcpp
+{
+
+class List;
+
+}
+
+namespace scratchcpprender
+{
+
+class ListMonitorListModel : public QAbstractListModel
+{
+    public:
+        explicit ListMonitorListModel(QObject *parent = nullptr);
+
+        void setList(libscratchcpp::List *list);
+
+        int rowCount(const QModelIndex &parent) const override;
+        QVariant data(const QModelIndex &index, int role) const override;
+        QHash<int, QByteArray> roleNames() const override;
+
+    private:
+        libscratchcpp::List *m_list = nullptr;
+        int m_oldRowCount = 0;
+};
+
+} // namespace scratchcpprender
diff --git a/src/listmonitormodel.cpp b/src/listmonitormodel.cpp
new file mode 100644
index 0000000..67f920a
--- /dev/null
+++ b/src/listmonitormodel.cpp
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <scratchcpp/iblocksection.h>
+#include <scratchcpp/virtualmachine.h>
+
+#include "listmonitormodel.h"
+#include "listmonitorlistmodel.h"
+
+using namespace scratchcpprender;
+
+ListMonitorModel::ListMonitorModel(QObject *parent) :
+    MonitorModel(parent)
+{
+    m_listModel = new ListMonitorListModel(this);
+}
+
+ListMonitorModel::ListMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent) :
+    ListMonitorModel(parent)
+{
+    if (!section)
+        return;
+
+    // TODO: Get the color from the block section
+    std::string name = section->name();
+    if (name == "Motion")
+        m_color = QColor::fromString("#4C97FF");
+    else if (name == "Looks")
+        m_color = QColor::fromString("#9966FF");
+    else if (name == "Sound")
+        m_color = QColor::fromString("#CF63CF");
+    else if (name == "Sensing")
+        m_color = QColor::fromString("#5CB1D6");
+    else if (name == "Variables")
+        m_color = QColor::fromString("#FF8C1A");
+    else if (name == "Lists")
+        m_color = QColor::fromString("#FF661A");
+}
+
+void ListMonitorModel::onValueChanged(const libscratchcpp::VirtualMachine *vm)
+{
+    if (vm->registerCount() == 1) {
+        long index = vm->getInput(0, 1)->toLong();
+        libscratchcpp::List *list = vm->lists()[index];
+        m_listModel->setList(list);
+    }
+}
+
+MonitorModel::Type ListMonitorModel::type() const
+{
+    return Type::List;
+}
+
+const QColor &ListMonitorModel::color() const
+{
+    return m_color;
+}
+
+ListMonitorListModel *ListMonitorModel::listModel() const
+{
+    return m_listModel;
+}
diff --git a/src/listmonitormodel.h b/src/listmonitormodel.h
new file mode 100644
index 0000000..2afe1d1
--- /dev/null
+++ b/src/listmonitormodel.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <QColor>
+
+#include "monitormodel.h"
+
+Q_MOC_INCLUDE("listmonitorlistmodel.h")
+
+namespace libscratchcpp
+{
+
+class IBlockSection;
+
+}
+
+namespace scratchcpprender
+{
+
+class ListMonitorListModel;
+
+class ListMonitorModel : public MonitorModel
+{
+        Q_OBJECT
+        QML_ELEMENT
+        Q_PROPERTY(QColor color READ color NOTIFY colorChanged)
+        Q_PROPERTY(ListMonitorListModel *listModel READ listModel NOTIFY listModelChanged)
+
+    public:
+        ListMonitorModel(QObject *parent = nullptr);
+        ListMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent = nullptr);
+
+        void onValueChanged(const libscratchcpp::VirtualMachine *vm) override;
+
+        Type type() const override;
+
+        const QColor &color() const;
+
+        ListMonitorListModel *listModel() const;
+
+    signals:
+        void colorChanged();
+        void listModelChanged();
+
+    private:
+        QColor m_color = Qt::green;
+        ListMonitorListModel *m_listModel = nullptr;
+};
+
+} // namespace scratchcpprender
diff --git a/src/monitormodel.cpp b/src/monitormodel.cpp
new file mode 100644
index 0000000..5ff9d8c
--- /dev/null
+++ b/src/monitormodel.cpp
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <scratchcpp/monitor.h>
+#include <scratchcpp/sprite.h>
+
+#include "monitormodel.h"
+
+using namespace scratchcpprender;
+
+MonitorModel::MonitorModel(QObject *parent) :
+    QObject(parent)
+{
+}
+
+QString MonitorModel::name() const
+{
+    if (m_monitor) {
+        if (libscratchcpp::Sprite *sprite = m_monitor->sprite())
+            return QString::fromStdString(sprite->name() + ": " + m_monitor->name());
+        else
+            return QString::fromStdString(m_monitor->name());
+    } else
+        return "";
+}
+
+bool MonitorModel::visible() const
+{
+    if (m_monitor)
+        return m_monitor->visible();
+    else
+        return false;
+}
+
+void MonitorModel::init(libscratchcpp::Monitor *monitor)
+{
+    m_monitor = monitor;
+}
+
+void MonitorModel::onVisibleChanged(bool visible)
+{
+    emit visibleChanged();
+}
+
+libscratchcpp::Monitor *MonitorModel::monitor() const
+{
+    return m_monitor;
+}
+
+int MonitorModel::x() const
+{
+    if (m_monitor)
+        return m_monitor->x();
+    else
+        return 0;
+}
+
+int MonitorModel::y() const
+{
+    if (m_monitor)
+        return m_monitor->y();
+    else
+        return 0;
+}
+
+unsigned int MonitorModel::width() const
+{
+    if (m_monitor)
+        return m_monitor->width();
+    else
+        return 0;
+}
+
+void MonitorModel::setWidth(unsigned int newWidth)
+{
+    if (m_monitor) {
+        m_monitor->setWidth(newWidth);
+        emit widthChanged();
+    }
+}
+
+unsigned int MonitorModel::height() const
+{
+    if (m_monitor)
+        return m_monitor->height();
+    else
+        return 0;
+}
+
+void MonitorModel::setHeight(unsigned int newHeight)
+{
+    if (m_monitor) {
+        m_monitor->setHeight(newHeight);
+        emit heightChanged();
+    }
+}
diff --git a/src/monitormodel.h b/src/monitormodel.h
new file mode 100644
index 0000000..29c7ba1
--- /dev/null
+++ b/src/monitormodel.h
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QQmlEngine>
+#include <scratchcpp/imonitorhandler.h>
+
+namespace scratchcpprender
+{
+
+class MonitorModel
+    : public QObject
+    , public libscratchcpp::IMonitorHandler
+{
+        Q_OBJECT
+        QML_ELEMENT
+        Q_PROPERTY(QString name READ name NOTIFY nameChanged)
+        Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged)
+        Q_PROPERTY(Type type READ type NOTIFY typeChanged)
+        Q_PROPERTY(int x READ x NOTIFY xChanged)
+        Q_PROPERTY(int y READ y NOTIFY yChanged)
+        Q_PROPERTY(unsigned int width READ width WRITE setWidth NOTIFY widthChanged)
+        Q_PROPERTY(unsigned int height READ height WRITE setHeight NOTIFY heightChanged)
+
+    public:
+        enum class Type
+        {
+            Invalid,
+            Value,
+            List
+        };
+
+        Q_ENUM(Type)
+
+        MonitorModel(QObject *parent = nullptr);
+
+        void init(libscratchcpp::Monitor *monitor) override final;
+
+        virtual void onValueChanged(const libscratchcpp::VirtualMachine *vm) override { }
+        void onVisibleChanged(bool visible) override final;
+
+        libscratchcpp::Monitor *monitor() const;
+
+        QString name() const;
+
+        bool visible() const;
+
+        virtual Type type() const { return Type::Invalid; }
+
+        int x() const;
+
+        int y() const;
+
+        unsigned int width() const;
+        void setWidth(unsigned int newWidth);
+
+        unsigned int height() const;
+        void setHeight(unsigned int newHeight);
+
+    signals:
+        void visibleChanged();
+        void typeChanged();
+        void xChanged();
+        void yChanged();
+        void widthChanged();
+        void heightChanged();
+        void nameChanged();
+
+    private:
+        libscratchcpp::Monitor *m_monitor = nullptr;
+};
+
+} // namespace scratchcpprender
diff --git a/src/mouseeventhandler.cpp b/src/mouseeventhandler.cpp
new file mode 100644
index 0000000..571771d
--- /dev/null
+++ b/src/mouseeventhandler.cpp
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <QMouseEvent>
+#include <scratchcpp/target.h>
+
+#include "mouseeventhandler.h"
+#include "renderedtarget.h"
+#include "projectloader.h"
+#include "spritemodel.h"
+
+using namespace scratchcpprender;
+
+MouseEventHandler::MouseEventHandler(QObject *parent) :
+    QObject(parent)
+{
+}
+
+IRenderedTarget *MouseEventHandler::stage() const
+{
+    return m_stage;
+}
+
+void MouseEventHandler::setStage(IRenderedTarget *stage)
+{
+    m_stage = stage;
+}
+
+ProjectLoader *MouseEventHandler::projectLoader() const
+{
+    return m_projectLoader;
+}
+
+void MouseEventHandler::setProjectLoader(ProjectLoader *newProjectLoader)
+{
+    m_projectLoader = newProjectLoader;
+
+    if (m_projectLoader) {
+        connect(m_projectLoader, &ProjectLoader::spritesChanged, this, &MouseEventHandler::getSprites);
+        connect(m_projectLoader, &ProjectLoader::cloneCreated, this, &MouseEventHandler::addClone);
+        connect(m_projectLoader, &ProjectLoader::cloneDeleted, this, &MouseEventHandler::removeClone);
+    }
+}
+
+bool MouseEventHandler::eventFilter(QObject *obj, QEvent *event)
+{
+    switch (event->type()) {
+        case QEvent::HoverEnter:
+        case QEvent::HoverLeave:
+            forwardPointEvent(static_cast<QSinglePointEvent *>(event));
+            return true;
+
+        case QEvent::HoverMove:
+        case QEvent::MouseMove: {
+            QSinglePointEvent *pointEvent = static_cast<QSinglePointEvent *>(event);
+            forwardPointEvent(pointEvent);
+            QPointF pos = pointEvent->position();
+            emit mouseMoved(pos.x(), pos.y());
+            return true;
+        }
+
+        case QEvent::MouseButtonPress:
+            emit mousePressed();
+            forwardPointEvent(static_cast<QSinglePointEvent *>(event));
+            return true;
+
+        case QEvent::MouseButtonRelease: {
+            emit mouseReleased();
+            QQuickItem *oldClickedItem = m_clickedItem;
+
+            if (m_clickedItem) {
+                sendPointEventToItem(static_cast<QSinglePointEvent *>(event), m_clickedItem);
+                m_clickedItem = nullptr;
+            }
+
+            forwardPointEvent(static_cast<QSinglePointEvent *>(event), oldClickedItem);
+
+            return true;
+        }
+
+        default:
+            break;
+    }
+
+    return QObject::eventFilter(obj, event);
+}
+
+void scratchcpprender::MouseEventHandler::getSprites()
+{
+    Q_ASSERT(m_projectLoader);
+
+    if (!m_projectLoader)
+        return;
+
+    m_sprites.clear();
+    const auto &spriteModels = m_projectLoader->spriteList();
+
+    for (SpriteModel *model : spriteModels) {
+        Q_ASSERT(model);
+        IRenderedTarget *sprite = model->renderedTarget();
+        Q_ASSERT(sprite);
+        Q_ASSERT(sprite->scratchTarget());
+        m_sprites.push_back(sprite);
+    }
+}
+
+void scratchcpprender::MouseEventHandler::addClone(SpriteModel *model)
+{
+    Q_ASSERT(model);
+    IRenderedTarget *sprite = model->renderedTarget();
+    Q_ASSERT(sprite);
+    Q_ASSERT(std::find_if(m_sprites.begin(), m_sprites.end(), [sprite](IRenderedTarget *renderedTarget) { return renderedTarget == sprite; }) == m_sprites.end());
+    m_sprites.push_back(sprite);
+}
+
+void scratchcpprender::MouseEventHandler::removeClone(SpriteModel *model)
+{
+    Q_ASSERT(model);
+    IRenderedTarget *sprite = model->renderedTarget();
+    Q_ASSERT(sprite);
+    m_sprites.erase(std::remove_if(m_sprites.begin(), m_sprites.end(), [sprite](IRenderedTarget *renderedTarget) { return renderedTarget == sprite; }), m_sprites.end());
+    Q_ASSERT(std::find_if(m_sprites.begin(), m_sprites.end(), [sprite](IRenderedTarget *renderedTarget) { return renderedTarget == sprite; }) == m_sprites.end());
+
+    // Make sure the pointer is never used again after it becomes "dangling"
+    if (m_clickedItem == sprite)
+        m_clickedItem = nullptr;
+
+    if (m_hoveredItem == sprite)
+        m_hoveredItem = nullptr;
+}
+
+void MouseEventHandler::forwardPointEvent(QSinglePointEvent *event, QQuickItem *oldClickedItem)
+{
+    Q_ASSERT(m_projectLoader);
+
+    if (!m_projectLoader)
+        return;
+
+    // Sort sprite list by layer order
+    std::sort(m_sprites.begin(), m_sprites.end(), [](IRenderedTarget *t1, IRenderedTarget *t2) { return t1->scratchTarget()->layerOrder() > t2->scratchTarget()->layerOrder(); });
+
+    // Find hovered sprite
+    QQuickItem *hoveredItem = nullptr;
+
+    for (IRenderedTarget *sprite : m_sprites) {
+        // contains() expects position in the item's coordinate system
+        QPointF localPos = sprite->mapFromScene(event->scenePosition());
+
+        if (sprite->contains(localPos)) {
+            hoveredItem = sprite;
+            break;
+        }
+    }
+
+    // If there wasn't any hovered sprite, send the event to the stage
+    if (!hoveredItem) {
+        hoveredItem = m_stage;
+        Q_ASSERT(m_stage);
+    }
+
+    // Send the event to the item
+    if (hoveredItem) {
+        // Since both the hovered item and previously clicked item should receive mouse release event,
+        // avoid duplicate events by checking whether the previously clicked item is the hovered item.
+        if (!(event->type() == QEvent::MouseButtonRelease && hoveredItem == oldClickedItem)) {
+            sendPointEventToItem(event, hoveredItem);
+        }
+    }
+}
+
+void MouseEventHandler::sendPointEventToItem(QSinglePointEvent *event, QQuickItem *item)
+{
+    Q_ASSERT(event);
+    Q_ASSERT(item);
+    Q_ASSERT(m_stage);
+
+    switch (event->type()) {
+        case QEvent::HoverEnter:
+        case QEvent::HoverLeave:
+            QCoreApplication::sendEvent(item, event);
+            break;
+
+        case QEvent::HoverMove:
+            // Send HoverLeave to the previously hovered item and HoverEnter to the hovered item
+            if (item != m_hoveredItem) {
+                if (m_hoveredItem)
+                    sendHoverEventToItem(static_cast<QHoverEvent *>(event), QEvent::HoverLeave, m_hoveredItem);
+
+                sendHoverEventToItem(static_cast<QHoverEvent *>(event), QEvent::HoverEnter, item);
+                m_hoveredItem = item;
+            }
+
+            QCoreApplication::sendEvent(item, event);
+            break;
+
+        case QEvent::MouseButtonPress:
+        case QEvent::MouseButtonRelease:
+        case QEvent::MouseMove: {
+            if (event->type() == QEvent::MouseButtonPress)
+                m_clickedItem = item;
+
+            QMouseEvent itemEvent(
+                event->type(),
+                item->mapFromScene(event->scenePosition()),
+                event->scenePosition(),
+                event->globalPosition(),
+                event->button(),
+                event->buttons(),
+                event->modifiers(),
+                static_cast<const QPointingDevice *>(event->device()));
+
+            QCoreApplication::sendEvent(item, &itemEvent);
+            break;
+        }
+
+        default:
+            Q_ASSERT(false);
+            break;
+    }
+}
+
+void MouseEventHandler::sendHoverEventToItem(QHoverEvent *originalEvent, QEvent::Type newType, QQuickItem *item)
+{
+    Q_ASSERT(newType == QEvent::HoverEnter || newType == QEvent::HoverLeave || newType == QEvent::HoverMove);
+
+    QHoverEvent event(
+        newType,
+        originalEvent->scenePosition(),
+        originalEvent->globalPosition(),
+        originalEvent->oldPosF(),
+        originalEvent->modifiers(),
+        static_cast<const QPointingDevice *>(originalEvent->device()));
+
+    QCoreApplication::sendEvent(item, &event);
+}
diff --git a/src/mouseeventhandler.h b/src/mouseeventhandler.h
new file mode 100644
index 0000000..407a0e5
--- /dev/null
+++ b/src/mouseeventhandler.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QEvent>
+
+class QQuickItem;
+class QSinglePointEvent;
+class QHoverEvent;
+
+namespace scratchcpprender
+{
+
+class IRenderedTarget;
+class ProjectLoader;
+class SpriteModel;
+
+class MouseEventHandler : public QObject
+{
+        Q_OBJECT
+    public:
+        explicit MouseEventHandler(QObject *parent = nullptr);
+
+        IRenderedTarget *stage() const;
+        void setStage(IRenderedTarget *stage);
+
+        bool eventFilter(QObject *obj, QEvent *event) override;
+
+        ProjectLoader *projectLoader() const;
+        void setProjectLoader(ProjectLoader *newProjectLoader);
+
+    signals:
+        void mouseMoved(qreal x, qreal y);
+        void mousePressed();
+        void mouseReleased();
+
+    private:
+        void getSprites();
+        void addClone(SpriteModel *model);
+        void removeClone(SpriteModel *model);
+        void forwardPointEvent(QSinglePointEvent *event, QQuickItem *oldClickedItem = nullptr);
+        void sendPointEventToItem(QSinglePointEvent *event, QQuickItem *item);
+        void sendHoverEventToItem(QHoverEvent *originalEvent, QEvent::Type newType, QQuickItem *item);
+
+        IRenderedTarget *m_stage = nullptr;
+        std::vector<IRenderedTarget *> m_sprites;
+        QQuickItem *m_hoveredItem = nullptr;
+        QQuickItem *m_clickedItem = nullptr;
+        ProjectLoader *m_projectLoader = nullptr;
+};
+
+} // namespace scratchcpprender
diff --git a/ScratchCPPGui/projectloader.cpp b/src/projectloader.cpp
similarity index 71%
rename from ScratchCPPGui/projectloader.cpp
rename to src/projectloader.cpp
index d3c5832..fe700ec 100644
--- a/ScratchCPPGui/projectloader.cpp
+++ b/src/projectloader.cpp
@@ -2,21 +2,19 @@
 
 #include <scratchcpp/iengine.h>
 #include <scratchcpp/value.h>
+#include <scratchcpp/monitor.h>
 #include <QtConcurrent/QtConcurrent>
 #include <QApplication>
 
 #include "projectloader.h"
 #include "spritemodel.h"
+#include "valuemonitormodel.h"
+#include "listmonitormodel.h"
 #include "renderedtarget.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 using namespace libscratchcpp;
 
-void runEventLoop(IEngine *engine)
-{
-    engine->runEventLoop();
-}
-
 ProjectLoader::ProjectLoader(QObject *parent) :
     QObject(parent)
 {
@@ -33,12 +31,6 @@ ProjectLoader::ProjectLoader(QObject *parent) :
     });
 
     initTimer();
-
-    // Update refresh rate when primary screen changes
-    connect(qApp, &QApplication::primaryScreenChanged, this, [this]() {
-        killTimer(m_timerId);
-        initTimer();
-    });
 }
 
 ProjectLoader::~ProjectLoader()
@@ -48,11 +40,6 @@ ProjectLoader::~ProjectLoader()
     if (m_loadThread.isRunning())
         m_loadThread.waitForFinished();
 
-    if (m_engine && m_eventLoopEnabled) {
-        m_engine->stopEventLoop();
-        m_eventLoop.waitForFinished();
-    }
-
     for (SpriteModel *sprite : m_sprites)
         sprite->deleteLater();
 }
@@ -72,11 +59,6 @@ void ProjectLoader::setFileName(const QString &newFileName)
 
     m_fileName = newFileName;
 
-    if (m_engine) {
-        m_engine->stopEventLoop();
-        m_eventLoop.waitForFinished();
-    }
-
     m_project.setScratchVersion(ScratchVersion::Scratch3);
     m_project.setFileName(m_fileName.toStdString());
     m_loadStatus = false;
@@ -137,6 +119,26 @@ const QList<SpriteModel *> &ProjectLoader::spriteList() const
     return m_sprites;
 }
 
+QQmlListProperty<SpriteModel> ProjectLoader::clones()
+{
+    return QQmlListProperty<SpriteModel>(this, &m_clones);
+}
+
+const QList<SpriteModel *> &ProjectLoader::cloneList() const
+{
+    return m_clones;
+}
+
+QQmlListProperty<MonitorModel> ProjectLoader::monitors()
+{
+    return QQmlListProperty<MonitorModel>(this, &m_monitors);
+}
+
+const QList<MonitorModel *> &ProjectLoader::monitorList() const
+{
+    return m_monitors;
+}
+
 void ProjectLoader::start()
 {
     if (m_loadThread.isRunning())
@@ -164,17 +166,8 @@ void ProjectLoader::timerEvent(QTimerEvent *event)
     if (m_loadThread.isRunning())
         return;
 
-    auto stageRenderedTarget = m_stage.renderedTarget();
-
-    if (stageRenderedTarget)
-        stageRenderedTarget->updateProperties();
-
-    for (auto sprite : m_sprites) {
-        auto renderedTarget = sprite->renderedTarget();
-
-        if (renderedTarget)
-            renderedTarget->updateProperties();
-    }
+    if (m_engine)
+        m_engine->step();
 
     event->accept();
 }
@@ -213,8 +206,14 @@ void ProjectLoader::load()
     m_engine->setCloneLimit(m_cloneLimit);
     m_engine->setSpriteFencingEnabled(m_spriteFencing);
 
-    auto handler = std::bind(&ProjectLoader::emitTick, this);
-    m_engine->setRedrawHandler(std::function<void()>(handler));
+    auto redrawHandler = std::bind(&ProjectLoader::redraw, this);
+    m_engine->setRedrawHandler(std::function<void()>(redrawHandler));
+
+    auto addMonitorHandler = std::bind(&ProjectLoader::addMonitor, this, std::placeholders::_1);
+    m_engine->setAddMonitorHandler(std::function<void(Monitor *)>(addMonitorHandler));
+
+    auto removeMonitorHandler = std::bind(&ProjectLoader::removeMonitor, this, std::placeholders::_1, std::placeholders::_2);
+    m_engine->setRemoveMonitorHandler(std::function<void(Monitor *, IMonitorHandler *)>(removeMonitorHandler));
 
     // Load targets
     const auto &targets = m_engine->targets();
@@ -226,10 +225,17 @@ void ProjectLoader::load()
             SpriteModel *sprite = new SpriteModel;
             sprite->moveToThread(qApp->thread());
             dynamic_cast<Sprite *>(target.get())->setInterface(sprite);
+            connect(sprite, &SpriteModel::cloned, this, &ProjectLoader::addClone);
             m_sprites.push_back(sprite);
         }
     }
 
+    // Load monitors
+    const auto &monitors = m_engine->monitors();
+
+    for (auto monitor : monitors)
+        addMonitor(monitor.get());
+
     if (m_stopLoading) {
         m_engineMutex.unlock();
         emit fileNameChanged();
@@ -240,12 +246,6 @@ void ProjectLoader::load()
         return;
     }
 
-    // Run event loop
-    m_engine->setSpriteFencingEnabled(false);
-
-    if (m_eventLoopEnabled)
-        m_eventLoop = QtConcurrent::run(&runEventLoop, m_engine);
-
     m_engineMutex.unlock();
 
     emit loadStatusChanged();
@@ -257,28 +257,94 @@ void ProjectLoader::load()
 
 void ProjectLoader::initTimer()
 {
-    QScreen *screen = qApp->primaryScreen();
-
-    if (screen)
-        m_timerId = startTimer(1000 / screen->refreshRate());
+    m_timerId = startTimer(1000 / m_fps);
 }
 
-void ProjectLoader::emitTick()
+void ProjectLoader::redraw()
 {
     if (m_loadThread.isRunning())
         m_loadThread.waitForFinished();
 
-    auto stageRenderedTarget = m_stage.renderedTarget();
+    auto stage = m_stage.renderedTarget();
 
-    if (stageRenderedTarget)
-        stageRenderedTarget->loadProperties();
+    if (stage)
+        stage->beforeRedraw();
 
     for (auto sprite : m_sprites) {
         auto renderedTarget = sprite->renderedTarget();
 
         if (renderedTarget)
-            renderedTarget->loadProperties();
+            renderedTarget->beforeRedraw();
+    }
+
+    for (auto sprite : m_clones) {
+        auto renderedTarget = sprite->renderedTarget();
+
+        if (renderedTarget)
+            renderedTarget->beforeRedraw();
     }
+
+    m_engine->updateMonitors();
+}
+
+void ProjectLoader::addClone(SpriteModel *model)
+{
+    connect(model, &SpriteModel::cloneDeleted, this, &ProjectLoader::deleteClone);
+    m_clones.push_back(model);
+    emit cloneCreated(model);
+    emit clonesChanged();
+}
+
+void ProjectLoader::deleteClone(SpriteModel *model)
+{
+    m_clones.removeAll(model);
+    emit cloneDeleted(model);
+    Q_ASSERT(model->renderedTarget());
+    model->renderedTarget()->deinitClone();
+    model->deleteLater();
+    emit clonesChanged();
+}
+
+void ProjectLoader::addMonitor(Monitor *monitor)
+{
+    auto section = monitor->blockSection();
+
+    if (!section)
+        return;
+
+    MonitorModel *model = nullptr;
+
+    switch (monitor->mode()) {
+        case Monitor::Mode::List:
+            model = new ListMonitorModel(section.get());
+            break;
+
+        default:
+            model = new ValueMonitorModel(section.get());
+            break;
+    }
+
+    if (!model)
+        return;
+
+    model->moveToThread(qApp->thread());
+    model->setParent(this);
+    monitor->setInterface(model);
+    m_monitors.push_back(model);
+    emit monitorAdded(model);
+    emit monitorsChanged();
+}
+
+void ProjectLoader::removeMonitor(Monitor *monitor, IMonitorHandler *iface)
+{
+    MonitorModel *model = dynamic_cast<MonitorModel *>(iface);
+
+    if (!model)
+        return;
+
+    m_monitors.removeAll(model);
+    emit monitorRemoved(model);
+    emit monitorsChanged();
 }
 
 double ProjectLoader::fps() const
@@ -299,6 +365,9 @@ void ProjectLoader::setFps(double newFps)
     } else
         m_fps = newFps;
 
+    killTimer(m_timerId);
+    initTimer();
+
     m_engineMutex.unlock();
     emit fpsChanged();
 }
@@ -405,32 +474,6 @@ void ProjectLoader::setSpriteFencing(bool newSpriteFencing)
     emit spriteFencingChanged();
 }
 
-bool ProjectLoader::eventLoopEnabled() const
-{
-    return m_eventLoopEnabled;
-}
-
-void ProjectLoader::setEventLoopEnabled(bool newEventLoopEnabled)
-{
-    if (m_eventLoopEnabled == newEventLoopEnabled)
-        return;
-
-    m_eventLoopEnabled = newEventLoopEnabled;
-    m_engineMutex.lock();
-
-    if (m_engine) {
-        if (m_eventLoopEnabled)
-            m_eventLoop = QtConcurrent::run(&runEventLoop, m_engine);
-        else {
-            m_engine->stopEventLoop();
-            m_eventLoop.waitForFinished();
-        }
-    }
-
-    m_engineMutex.unlock();
-    emit eventLoopEnabledChanged();
-}
-
 unsigned int ProjectLoader::downloadedAssets() const
 {
     return m_downloadedAssets;
diff --git a/ScratchCPPGui/projectloader.h b/src/projectloader.h
similarity index 77%
rename from ScratchCPPGui/projectloader.h
rename to src/projectloader.h
index 3c49da8..06ac070 100644
--- a/ScratchCPPGui/projectloader.h
+++ b/src/projectloader.h
@@ -9,10 +9,14 @@
 
 #include "stagemodel.h"
 
-namespace scratchcppgui
+Q_MOC_INCLUDE("spritemodel.h");
+Q_MOC_INCLUDE("monitormodel.h");
+
+namespace scratchcpprender
 {
 
 class SpriteModel;
+class MonitorModel;
 
 class ProjectLoader : public QObject
 {
@@ -23,13 +27,14 @@ class ProjectLoader : public QObject
         Q_PROPERTY(libscratchcpp::IEngine *engine READ engine NOTIFY engineChanged)
         Q_PROPERTY(StageModel *stage READ stage NOTIFY stageChanged)
         Q_PROPERTY(QQmlListProperty<SpriteModel> sprites READ sprites NOTIFY spritesChanged)
+        Q_PROPERTY(QQmlListProperty<SpriteModel> clones READ clones NOTIFY clonesChanged)
+        Q_PROPERTY(QQmlListProperty<MonitorModel> monitors READ monitors NOTIFY monitorsChanged)
         Q_PROPERTY(double fps READ fps WRITE setFps NOTIFY fpsChanged)
         Q_PROPERTY(bool turboMode READ turboMode WRITE setTurboMode NOTIFY turboModeChanged)
         Q_PROPERTY(unsigned int stageWidth READ stageWidth WRITE setStageWidth NOTIFY stageWidthChanged)
         Q_PROPERTY(unsigned int stageHeight READ stageHeight WRITE setStageHeight NOTIFY stageHeightChanged)
         Q_PROPERTY(int cloneLimit READ cloneLimit WRITE setCloneLimit NOTIFY cloneLimitChanged)
         Q_PROPERTY(bool spriteFencing READ spriteFencing WRITE setSpriteFencing NOTIFY spriteFencingChanged)
-        Q_PROPERTY(bool eventLoopEnabled READ eventLoopEnabled WRITE setEventLoopEnabled NOTIFY eventLoopEnabledChanged)
         Q_PROPERTY(unsigned int downloadedAssets READ downloadedAssets NOTIFY downloadedAssetsChanged)
         Q_PROPERTY(unsigned int assetCount READ assetCount NOTIFY assetCountChanged)
 
@@ -50,6 +55,12 @@ class ProjectLoader : public QObject
         QQmlListProperty<SpriteModel> sprites();
         const QList<SpriteModel *> &spriteList() const;
 
+        QQmlListProperty<SpriteModel> clones();
+        const QList<SpriteModel *> &cloneList() const;
+
+        QQmlListProperty<MonitorModel> monitors();
+        const QList<MonitorModel *> &monitorList() const;
+
         Q_INVOKABLE void start();
         Q_INVOKABLE void stop();
 
@@ -71,9 +82,6 @@ class ProjectLoader : public QObject
         bool spriteFencing() const;
         void setSpriteFencing(bool newSpriteFencing);
 
-        bool eventLoopEnabled() const;
-        void setEventLoopEnabled(bool newEventLoopEnabled);
-
         unsigned int downloadedAssets() const;
 
         unsigned int assetCount() const;
@@ -85,15 +93,20 @@ class ProjectLoader : public QObject
         void engineChanged();
         void stageChanged();
         void spritesChanged();
+        void clonesChanged();
+        void monitorsChanged();
         void fpsChanged();
         void turboModeChanged();
         void stageWidthChanged();
         void stageHeightChanged();
         void cloneLimitChanged();
         void spriteFencingChanged();
-        void eventLoopEnabledChanged();
         void downloadedAssetsChanged();
         void assetCountChanged();
+        void cloneCreated(SpriteModel *model);
+        void cloneDeleted(SpriteModel *model);
+        void monitorAdded(MonitorModel *model);
+        void monitorRemoved(MonitorModel *model);
 
     protected:
         void timerEvent(QTimerEvent *event) override;
@@ -102,7 +115,11 @@ class ProjectLoader : public QObject
         static void callLoad(ProjectLoader *loader);
         void load();
         void initTimer();
-        void emitTick();
+        void redraw();
+        void addClone(SpriteModel *model);
+        void deleteClone(SpriteModel *model);
+        void addMonitor(libscratchcpp::Monitor *monitor);
+        void removeMonitor(libscratchcpp::Monitor *monitor, libscratchcpp::IMonitorHandler *iface);
 
         int m_timerId = -1;
         QString m_fileName;
@@ -113,17 +130,17 @@ class ProjectLoader : public QObject
         bool m_loadStatus = false;
         StageModel m_stage;
         QList<SpriteModel *> m_sprites;
-        QFuture<void> m_eventLoop;
+        QList<SpriteModel *> m_clones;
+        QList<MonitorModel *> m_monitors;
         double m_fps = 30;
         bool m_turboMode = false;
         unsigned int m_stageWidth = 480;
         unsigned int m_stageHeight = 360;
         int m_cloneLimit = 300;
         bool m_spriteFencing = true;
-        bool m_eventLoopEnabled = true;
         std::atomic<unsigned int> m_downloadedAssets = 0;
         std::atomic<unsigned int> m_assetCount = 0;
         std::atomic<bool> m_stopLoading = false;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/ScratchCPPGui/projectscene.cpp b/src/projectscene.cpp
similarity index 57%
rename from ScratchCPPGui/projectscene.cpp
rename to src/projectscene.cpp
index 5a0ddc8..fc58ce8 100644
--- a/ScratchCPPGui/projectscene.cpp
+++ b/src/projectscene.cpp
@@ -7,7 +7,7 @@
 #include "projectscene.h"
 #include "keyeventhandler.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 using namespace libscratchcpp;
 
 static const std::unordered_map<Qt::Key, KeyEvent::Type> SPECIAL_KEY_MAP = {
@@ -37,11 +37,25 @@ void ProjectScene::setEngine(IEngine *newEngine)
     emit engineChanged();
 }
 
+double ProjectScene::stageScale() const
+{
+    return m_stageScale;
+}
+
+void ProjectScene::setStageScale(double newStageScale)
+{
+    if (qFuzzyCompare(m_stageScale, newStageScale))
+        return;
+
+    m_stageScale = newStageScale;
+    emit stageScaleChanged();
+}
+
 void ProjectScene::handleMouseMove(qreal x, qreal y)
 {
     if (m_engine) {
-        m_engine->setMouseX(x - m_engine->stageWidth() / 2.0);
-        m_engine->setMouseY(-y + m_engine->stageHeight() / 2.0);
+        m_engine->setMouseX(x / m_stageScale - m_engine->stageWidth() / 2.0);
+        m_engine->setMouseY(-y / m_stageScale + m_engine->stageHeight() / 2.0);
     }
 }
 
@@ -59,29 +73,42 @@ void ProjectScene::handleMouseRelease()
 
 void ProjectScene::handleKeyPress(Qt::Key key, const QString &text)
 {
-    if (m_engine) {
-        auto it = SPECIAL_KEY_MAP.find(key);
+    m_pressedKeys.insert(key);
 
-        if (it == SPECIAL_KEY_MAP.cend())
-            m_engine->setKeyState(text.toStdString(), true);
-        else {
-            KeyEvent event(it->second);
-            m_engine->setKeyState(event.name(), true);
+    if (m_engine) {
+        if (!text.isEmpty()) {
+            auto it = SPECIAL_KEY_MAP.find(key);
+
+            if (it == SPECIAL_KEY_MAP.cend())
+                m_engine->setKeyState(text.toStdString(), true);
+            else {
+                KeyEvent event(it->second);
+                m_engine->setKeyState(event.name(), true);
+            }
         }
+
+        m_engine->setAnyKeyPressed(!m_pressedKeys.empty());
     }
 }
 
 void ProjectScene::handleKeyRelease(Qt::Key key, const QString &text)
 {
-    if (m_engine) {
-        auto it = SPECIAL_KEY_MAP.find(key);
+    m_pressedKeys.erase(key);
 
-        if (it == SPECIAL_KEY_MAP.cend())
-            m_engine->setKeyState(text.toStdString(), false);
-        else {
-            KeyEvent event(it->second);
-            m_engine->setKeyState(event.name(), false);
+    if (m_engine) {
+        if (!text.isEmpty()) {
+            auto it = SPECIAL_KEY_MAP.find(key);
+
+            if (it == SPECIAL_KEY_MAP.cend())
+                m_engine->setKeyState(text.toStdString(), false);
+            else {
+                KeyEvent event(it->second);
+                m_engine->setKeyState(event.name(), false);
+            }
         }
+
+        if (m_pressedKeys.empty()) // avoid setting 'true' when a key is released
+            m_engine->setAnyKeyPressed(false);
     }
 }
 
diff --git a/ScratchCPPGui/projectscene.h b/src/projectscene.h
similarity index 72%
rename from ScratchCPPGui/projectscene.h
rename to src/projectscene.h
index 74fa311..96eda3a 100644
--- a/ScratchCPPGui/projectscene.h
+++ b/src/projectscene.h
@@ -3,13 +3,14 @@
 #pragma once
 
 #include <QQuickPaintedItem>
+#include <unordered_set>
 
 namespace libscratchcpp
 {
 class IEngine;
 }
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class KeyEventHandler;
@@ -19,6 +20,7 @@ class ProjectScene : public QQuickItem
         Q_OBJECT
         QML_ELEMENT
         Q_PROPERTY(libscratchcpp::IEngine *engine READ engine WRITE setEngine NOTIFY engineChanged)
+        Q_PROPERTY(double stageScale READ stageScale WRITE setStageScale NOTIFY stageScaleChanged)
 
     public:
         ProjectScene(QQuickItem *parent = nullptr);
@@ -26,6 +28,9 @@ class ProjectScene : public QQuickItem
         libscratchcpp::IEngine *engine() const;
         void setEngine(libscratchcpp::IEngine *newEngine);
 
+        double stageScale() const;
+        void setStageScale(double newStageScale);
+
         Q_INVOKABLE void handleMouseMove(qreal x, qreal y);
         Q_INVOKABLE void handleMousePress();
         Q_INVOKABLE void handleMouseRelease();
@@ -35,12 +40,15 @@ class ProjectScene : public QQuickItem
 
     signals:
         void engineChanged();
+        void stageScaleChanged();
 
     private:
         void installKeyHandler(QQuickWindow *window);
 
         libscratchcpp::IEngine *m_engine = nullptr;
+        double m_stageScale = 1;
         KeyEventHandler *m_keyHandler = nullptr;
+        std::unordered_set<Qt::Key> m_pressedKeys;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
new file mode 100644
index 0000000..0d3dfd8
--- /dev/null
+++ b/src/renderedtarget.cpp
@@ -0,0 +1,588 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <scratchcpp/iengine.h>
+#include <scratchcpp/costume.h>
+#include <QtSvg/QSvgRenderer>
+#include <qnanopainter.h>
+
+#include "renderedtarget.h"
+#include "targetpainter.h"
+#include "stagemodel.h"
+#include "spritemodel.h"
+#include "scenemousearea.h"
+
+using namespace scratchcpprender;
+using namespace libscratchcpp;
+
+static const double SVG_SCALE_LIMIT = 0.1; // the maximum viewport dimensions are multiplied by this
+
+RenderedTarget::RenderedTarget(QNanoQuickItem *parent) :
+    IRenderedTarget(parent)
+{
+    // Get maximum viewport dimensions
+    QOpenGLContext context;
+    context.create();
+    Q_ASSERT(context.isValid());
+
+    if (context.isValid()) {
+        QOffscreenSurface surface;
+        surface.create();
+        Q_ASSERT(surface.isValid());
+
+        if (surface.isValid()) {
+            context.makeCurrent(&surface);
+            GLint dims[2];
+            glGetIntegerv(GL_MAX_VIEWPORT_DIMS, dims);
+            m_maximumWidth = dims[0] * SVG_SCALE_LIMIT;
+            m_maximumHeight = dims[1] * SVG_SCALE_LIMIT;
+            context.doneCurrent();
+        }
+    }
+}
+
+void RenderedTarget::updateVisibility(bool visible)
+{
+    if (visible == isVisible())
+        return;
+
+    setVisible(visible);
+    calculatePos();
+}
+
+void RenderedTarget::updateX(double x)
+{
+    if (x == m_x)
+        return;
+
+    m_x = x;
+    calculatePos();
+}
+
+void RenderedTarget::updateY(double y)
+{
+    if (y == m_y)
+        return;
+
+    m_y = y;
+    calculatePos();
+}
+
+void RenderedTarget::updateSize(double size)
+{
+    if (size / 100 == m_size)
+        return;
+
+    m_size = size / 100;
+    calculateSize();
+    calculatePos();
+}
+
+void RenderedTarget::updateDirection(double direction)
+{
+    if (direction == m_direction)
+        return;
+
+    m_direction = direction;
+    calculateRotation();
+
+    if (m_rotationStyle == libscratchcpp::Sprite::RotationStyle::LeftRight)
+        calculatePos();
+}
+
+void RenderedTarget::updateRotationStyle(libscratchcpp::Sprite::RotationStyle style)
+{
+    if (style == m_rotationStyle)
+        return;
+
+    m_rotationStyle = style;
+    calculateRotation();
+    calculatePos();
+}
+
+void RenderedTarget::updateLayerOrder(int layerOrder)
+{
+    setZ(layerOrder);
+}
+
+void RenderedTarget::loadCostume(Costume *costume)
+{
+    if (!costume || costume == m_costume)
+        return;
+
+    m_costumeMutex.lock();
+    m_costume = costume;
+
+    if (m_costume->dataFormat() == "svg") {
+        m_svgRenderer.load(QByteArray::fromRawData(static_cast<const char *>(m_costume->data()), m_costume->dataSize()));
+        QRectF rect = m_svgRenderer.viewBoxF();
+        m_costumeWidth = rect.width();
+        m_costumeHeight = rect.height();
+    } else {
+        m_bitmapBuffer.open(QBuffer::WriteOnly);
+        m_bitmapBuffer.write(static_cast<const char *>(m_costume->data()), m_costume->dataSize());
+        m_bitmapBuffer.close();
+        m_bitmapUniqueKey = QString::fromStdString(m_costume->id());
+        const char *format;
+
+        {
+            QImageReader reader(&m_bitmapBuffer);
+            format = reader.format();
+        }
+
+        m_bitmapBuffer.close();
+        m_costumeBitmap.load(&m_bitmapBuffer, format);
+        QSize size = m_costumeBitmap.size();
+        m_costumeWidth = std::max(0, size.width());
+        m_costumeHeight = std::max(0, size.height());
+        m_bitmapBuffer.close();
+    }
+
+    m_costumeMutex.unlock();
+
+    calculateSize();
+    calculatePos();
+}
+
+void RenderedTarget::beforeRedraw()
+{
+    setWidth(m_width);
+    setHeight(m_height);
+}
+
+void RenderedTarget::deinitClone()
+{
+    // Do not process mouse move events after the clone has been deleted
+    disconnect(m_mouseArea, &SceneMouseArea::mouseMoved, this, &RenderedTarget::handleSceneMouseMove);
+
+    // Release drag lock
+    if (m_mouseArea->draggedSprite() == this)
+        m_mouseArea->setDraggedSprite(nullptr);
+}
+
+IEngine *RenderedTarget::engine() const
+{
+    return m_engine;
+}
+
+void RenderedTarget::setEngine(IEngine *newEngine)
+{
+    if (m_engine == newEngine)
+        return;
+
+    m_engine = newEngine;
+    emit engineChanged();
+}
+
+StageModel *RenderedTarget::stageModel() const
+{
+    return m_stageModel;
+}
+
+void RenderedTarget::setStageModel(StageModel *newStageModel)
+{
+    if (m_stageModel == newStageModel)
+        return;
+
+    m_stageModel = newStageModel;
+
+    if (m_stageModel) {
+        Stage *stage = m_stageModel->stage();
+
+        if (stage)
+            loadCostume(stage->currentCostume().get());
+    }
+
+    emit stageModelChanged();
+}
+
+SpriteModel *RenderedTarget::spriteModel() const
+{
+    return m_spriteModel;
+}
+
+void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel)
+{
+    if (m_spriteModel == newSpriteModel)
+        return;
+
+    m_spriteModel = newSpriteModel;
+
+    if (m_spriteModel) {
+        Sprite *sprite = m_spriteModel->sprite();
+
+        if (sprite) {
+            m_x = sprite->x();
+            m_y = sprite->y();
+            m_size = sprite->size() / 100;
+            m_direction = sprite->direction();
+            m_rotationStyle = sprite->rotationStyle();
+            loadCostume(sprite->currentCostume().get());
+            updateVisibility(sprite->visible());
+            updateLayerOrder(sprite->layerOrder());
+            calculateSize();
+            calculatePos();
+            calculateRotation();
+        }
+    }
+    emit spriteModelChanged();
+}
+
+Target *RenderedTarget::scratchTarget() const
+{
+    if (m_spriteModel)
+        return m_spriteModel->sprite();
+    else if (m_stageModel)
+        return m_stageModel->stage();
+    else
+        return nullptr;
+}
+
+SceneMouseArea *RenderedTarget::mouseArea() const
+{
+    return m_mouseArea;
+}
+
+void RenderedTarget::setMouseArea(SceneMouseArea *newMouseArea)
+{
+    if (m_mouseArea == newMouseArea)
+        return;
+
+    m_mouseArea = newMouseArea;
+    Q_ASSERT(m_mouseArea);
+    connect(m_mouseArea, &SceneMouseArea::mouseMoved, this, &RenderedTarget::handleSceneMouseMove);
+    emit mouseAreaChanged();
+}
+
+double RenderedTarget::stageScale() const
+{
+    return m_stageScale;
+}
+
+void RenderedTarget::setStageScale(double newStageScale)
+{
+    if (qFuzzyCompare(m_stageScale, newStageScale))
+        return;
+
+    m_stageScale = newStageScale;
+    calculateSize();
+    calculatePos();
+    emit stageScaleChanged();
+}
+
+qreal RenderedTarget::width() const
+{
+    return QNanoQuickItem::width();
+}
+
+void RenderedTarget::setWidth(qreal width)
+{
+    QNanoQuickItem::setWidth(width);
+}
+
+qreal RenderedTarget::height() const
+{
+    return QNanoQuickItem::height();
+}
+
+void RenderedTarget::setHeight(qreal height)
+{
+    QNanoQuickItem::setHeight(height);
+}
+
+QPointF RenderedTarget::mapFromScene(const QPointF &point) const
+{
+    return QNanoQuickItem::mapFromScene(point);
+}
+
+QNanoQuickItemPainter *RenderedTarget::createItemPainter() const
+{
+    return new TargetPainter();
+}
+
+void RenderedTarget::mousePressEvent(QMouseEvent *event)
+{
+    if (event->button() == Qt::LeftButton)
+        m_clicked = true;
+
+    if (m_engine && (!m_spriteModel || !m_spriteModel->sprite()->draggable())) {
+        // Notify libscratchcpp about the click
+        m_engine->clickTarget(scratchTarget());
+    }
+}
+
+void RenderedTarget::mouseReleaseEvent(QMouseEvent *event)
+{
+    m_clicked = false;
+    Q_ASSERT(m_mouseArea);
+    Q_ASSERT(m_engine);
+
+    // Stop dragging
+    if (m_mouseArea->draggedSprite() == this)
+        m_mouseArea->setDraggedSprite(nullptr);
+    else if (m_engine && m_spriteModel && m_spriteModel->sprite()->draggable()) {
+        // Notify libscratchcpp about the click
+        m_engine->clickTarget(scratchTarget());
+    }
+}
+
+void RenderedTarget::mouseMoveEvent(QMouseEvent *event)
+{
+    Q_ASSERT((m_spriteModel && m_spriteModel->sprite()) || m_stageModel);
+    Q_ASSERT(m_mouseArea);
+
+    // Start dragging
+    if (m_clicked && !m_mouseArea->draggedSprite() && m_spriteModel && m_spriteModel->sprite()->draggable()) {
+        Q_ASSERT(m_engine);
+        Sprite *sprite = m_spriteModel->sprite();
+        m_dragDeltaX = m_engine->mouseX() - sprite->x();
+        m_dragDeltaY = m_engine->mouseY() - sprite->y();
+        m_mouseArea->setDraggedSprite(this);
+
+        // Move the sprite to the front layer
+        m_engine->moveSpriteToFront(sprite);
+    }
+}
+
+void RenderedTarget::paintSvg(QNanoPainter *painter)
+{
+    Q_ASSERT(painter);
+    QOpenGLContext *context = QOpenGLContext::currentContext();
+    Q_ASSERT(context);
+
+    if (!context)
+        return;
+
+    QOffscreenSurface surface;
+    surface.setFormat(context->format());
+    surface.create();
+    Q_ASSERT(surface.isValid());
+
+    QSurface *oldSurface = context->surface();
+    context->makeCurrent(&surface);
+
+    const QRectF drawRect(0, 0, std::min(width(), m_maximumWidth), std::min(height(), m_maximumHeight));
+    const QSize drawRectSize = drawRect.size().toSize();
+
+    QOpenGLPaintDevice device(drawRectSize);
+    QPainter qPainter;
+    qPainter.begin(&device);
+    qPainter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
+    m_svgRenderer.render(&qPainter, drawRect);
+    qPainter.end();
+
+    context->doneCurrent();
+    context->makeCurrent(oldSurface);
+}
+
+void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo)
+{
+    if (m_stageModel)
+        return; // hull points are useless for the stage
+
+    Q_ASSERT(fbo);
+    int width = fbo->width();
+    int height = fbo->height();
+    m_hullPoints.clear();
+    m_hullPoints.reserve(width * height);
+
+    // Blit multisampled FBO to a custom FBO
+    QOpenGLFramebufferObject customFbo(fbo->size());
+    glBindFramebuffer(GL_READ_FRAMEBUFFER_EXT, fbo->handle());
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, customFbo.handle());
+    glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
+    glBindFramebuffer(GL_FRAMEBUFFER_EXT, customFbo.handle());
+
+    // Read pixels from framebuffer
+    size_t size = width * height * 4;
+    GLubyte *pixelData = new GLubyte[size];
+    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixelData);
+    glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0);
+    fbo->bind();
+
+    // Flip vertically
+    int rowSize = width * 4;
+    GLubyte *tempRow = new GLubyte[rowSize];
+
+    for (size_t i = 0; i < height / 2; ++i) {
+        size_t topRowIndex = i * rowSize;
+        size_t bottomRowIndex = (height - 1 - i) * rowSize;
+
+        // Swap rows
+        memcpy(tempRow, &pixelData[topRowIndex], rowSize);
+        memcpy(&pixelData[topRowIndex], &pixelData[bottomRowIndex], rowSize);
+        memcpy(&pixelData[bottomRowIndex], tempRow, rowSize);
+    }
+
+    delete[] tempRow;
+
+    // Fill hull points vector
+    for (int y = 0; y < height; y++) {
+        for (int x = 0; x < width; x++) {
+            int index = (y * width + x) * 4; // RGBA channels
+
+            // Check alpha channel
+            if (pixelData[index + 3] > 0)
+                m_hullPoints.push_back(QPointF(x, y));
+        }
+    }
+
+    delete[] pixelData;
+}
+
+const std::vector<QPointF> &RenderedTarget::hullPoints() const
+{
+    return m_hullPoints;
+}
+
+bool RenderedTarget::contains(const QPointF &point) const
+{
+    if (m_stageModel)
+        return true; // the stage contains any point within the scene
+
+    if (!boundingRect().contains(point))
+        return false;
+
+    QPoint intPoint = point.toPoint();
+    auto it = std::lower_bound(m_hullPoints.begin(), m_hullPoints.end(), intPoint, [](const QPointF &lhs, const QPointF &rhs) {
+        return (lhs.y() < rhs.y()) || (lhs.y() == rhs.y() && lhs.x() < rhs.x());
+    });
+
+    if (it == m_hullPoints.end()) {
+        // The point is beyond the last point in the convex hull
+        return false;
+    }
+
+    // Check if the point is equal to the one found
+    return *it == intPoint;
+}
+
+void RenderedTarget::calculatePos()
+{
+    if (!m_costume || !m_engine)
+        return;
+
+    if (m_spriteModel) {
+        if (isVisible()) {
+            double stageWidth = m_engine->stageWidth();
+            double stageHeight = m_engine->stageHeight();
+            setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_clampedSize / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1)));
+            setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_clampedSize / m_costume->bitmapResolution()));
+            qreal originX = m_costume->rotationCenterX() * m_clampedSize * m_stageScale / m_costume->bitmapResolution();
+            qreal originY = m_costume->rotationCenterY() * m_clampedSize * m_stageScale / m_costume->bitmapResolution();
+            setTransformOriginPoint(QPointF(originX, originY));
+        }
+    } else {
+        double stageWidth = m_engine->stageWidth();
+        double stageHeight = m_engine->stageHeight();
+        setX(m_stageScale * (stageWidth / 2 - m_costume->rotationCenterX() / m_costume->bitmapResolution()));
+        setY(m_stageScale * (stageHeight / 2 - m_costume->rotationCenterY() / m_costume->bitmapResolution()));
+        qreal originX = m_costume->rotationCenterX() / m_costume->bitmapResolution();
+        qreal originY = m_costume->rotationCenterY() / m_costume->bitmapResolution();
+        setTransformOriginPoint(QPointF(originX, originY));
+    }
+}
+
+void RenderedTarget::calculateRotation()
+{
+    if (isVisible()) {
+        // Direction
+        bool oldMirrorHorizontally = m_mirrorHorizontally;
+
+        switch (m_rotationStyle) {
+            case Sprite::RotationStyle::AllAround:
+                setRotation(m_direction - 90);
+                m_mirrorHorizontally = (false);
+
+                break;
+
+            case Sprite::RotationStyle::LeftRight: {
+                setRotation(0);
+                m_mirrorHorizontally = (m_direction < 0);
+
+                break;
+            }
+
+            case Sprite::RotationStyle::DoNotRotate:
+                setRotation(0);
+                m_mirrorHorizontally = false;
+                break;
+        }
+
+        if (m_mirrorHorizontally != oldMirrorHorizontally)
+            emit mirrorHorizontallyChanged();
+    }
+}
+
+void RenderedTarget::calculateSize()
+{
+    if (m_costume) {
+        double bitmapRes = m_costume->bitmapResolution();
+
+        if (m_costumeWidth == 0 || m_costumeHeight == 0)
+            m_maxSize = 1;
+        else
+            m_maxSize = std::min(m_maximumWidth / (m_costumeWidth * m_stageScale), m_maximumHeight / (m_costumeHeight * m_stageScale));
+
+        if (m_spriteModel) {
+            m_clampedSize = std::min(m_size, m_maxSize);
+            m_width = m_costumeWidth * m_clampedSize * m_stageScale / bitmapRes;
+            m_height = m_height = m_costumeHeight * m_clampedSize * m_stageScale / bitmapRes;
+        } else {
+            m_width = m_costumeWidth * m_stageScale / bitmapRes;
+            m_height = m_height = m_costumeHeight * m_stageScale / bitmapRes;
+        }
+    }
+
+    Q_ASSERT(m_maxSize > 0);
+
+    if (!m_stageModel && (m_size > m_maxSize) && (m_maxSize != 0))
+        setScale(m_size / m_maxSize);
+    else
+        setScale(1);
+}
+
+void RenderedTarget::handleSceneMouseMove(qreal x, qreal y)
+{
+    Q_ASSERT(m_mouseArea);
+
+    if (m_mouseArea->draggedSprite() == this) {
+        Q_ASSERT(m_spriteModel && m_spriteModel->sprite());
+        Q_ASSERT(m_engine);
+        Sprite *sprite = m_spriteModel->sprite();
+        sprite->setX(x / m_stageScale - m_engine->stageWidth() / 2.0 - m_dragDeltaX);
+        sprite->setY(-y / m_stageScale + m_engine->stageHeight() / 2.0 - m_dragDeltaY);
+    }
+}
+
+QBuffer *RenderedTarget::bitmapBuffer()
+{
+    return &m_bitmapBuffer;
+}
+
+const QString &RenderedTarget::bitmapUniqueKey() const
+{
+    return m_bitmapUniqueKey;
+}
+
+void RenderedTarget::lockCostume()
+{
+    m_costumeMutex.lock();
+}
+
+void RenderedTarget::unlockCostume()
+{
+    m_costumeMutex.unlock();
+}
+
+bool RenderedTarget::mirrorHorizontally() const
+{
+    return m_mirrorHorizontally;
+}
+
+bool RenderedTarget::isSvg() const
+{
+    if (!m_costume)
+        return false;
+
+    return (m_costume->dataFormat() == "svg");
+}
diff --git a/ScratchCPPGui/renderedtarget.h b/src/renderedtarget.h
similarity index 50%
rename from ScratchCPPGui/renderedtarget.h
rename to src/renderedtarget.h
index 9b8dffe..8cb92e9 100644
--- a/ScratchCPPGui/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -6,13 +6,15 @@
 #include <QBuffer>
 #include <QMutex>
 #include <QtSvg/QSvgRenderer>
+#include <QImage>
 
 #include "irenderedtarget.h"
 
 Q_MOC_INCLUDE("stagemodel.h");
 Q_MOC_INCLUDE("spritemodel.h");
+Q_MOC_INCLUDE("scenemousearea.h");
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class RenderedTarget : public IRenderedTarget
@@ -23,13 +25,25 @@ class RenderedTarget : public IRenderedTarget
         Q_PROPERTY(StageModel *stageModel READ stageModel WRITE setStageModel NOTIFY stageModelChanged)
         Q_PROPERTY(SpriteModel *spriteModel READ spriteModel WRITE setSpriteModel NOTIFY spriteModelChanged)
         Q_PROPERTY(bool mirrorHorizontally READ mirrorHorizontally NOTIFY mirrorHorizontallyChanged)
+        Q_PROPERTY(SceneMouseArea *mouseArea READ mouseArea WRITE setMouseArea NOTIFY mouseAreaChanged)
+        Q_PROPERTY(double stageScale READ stageScale WRITE setStageScale NOTIFY stageScaleChanged)
 
     public:
         RenderedTarget(QNanoQuickItem *parent = nullptr);
 
-        Q_INVOKABLE void loadProperties() override;
+        void updateVisibility(bool visible) override;
+        void updateX(double x) override;
+        void updateY(double y) override;
+        void updateSize(double size) override;
+        void updateDirection(double direction) override;
+        void updateRotationStyle(libscratchcpp::Sprite::RotationStyle style) override;
+        void updateLayerOrder(int layerOrder) override;
+
         void loadCostume(libscratchcpp::Costume *costume) override;
-        Q_INVOKABLE void updateProperties() override;
+
+        void beforeRedraw() override;
+
+        void deinitClone() override;
 
         libscratchcpp::IEngine *engine() const override;
         void setEngine(libscratchcpp::IEngine *newEngine) override;
@@ -42,12 +56,20 @@ class RenderedTarget : public IRenderedTarget
 
         libscratchcpp::Target *scratchTarget() const override;
 
+        SceneMouseArea *mouseArea() const override;
+        void setMouseArea(SceneMouseArea *newMouseArea) override;
+
+        double stageScale() const override;
+        void setStageScale(double newStageScale) override;
+
         qreal width() const override;
         void setWidth(qreal width) override;
 
         qreal height() const override;
         void setHeight(qreal height) override;
 
+        QPointF mapFromScene(const QPointF &point) const override;
+
         QBuffer *bitmapBuffer() override;
         const QString &bitmapUniqueKey() const override;
 
@@ -59,39 +81,61 @@ class RenderedTarget : public IRenderedTarget
         bool isSvg() const override;
         void paintSvg(QNanoPainter *painter) override;
 
+        void updateHullPoints(QOpenGLFramebufferObject *fbo) override;
+        const std::vector<QPointF> &hullPoints() const override;
+
+        Q_INVOKABLE bool contains(const QPointF &point) const override;
+
     signals:
         void engineChanged();
         void stageModelChanged();
         void spriteModelChanged();
-
+        void mouseAreaChanged();
         void mirrorHorizontallyChanged();
+        void stageScaleChanged();
 
     protected:
         QNanoQuickItemPainter *createItemPainter() const override;
+        void mousePressEvent(QMouseEvent *event) override;
+        void mouseReleaseEvent(QMouseEvent *event) override;
+        void mouseMoveEvent(QMouseEvent *event) override;
 
     private:
-        void doLoadCostume();
-        void calculateSize(libscratchcpp::Target *target, double costumeWidth, double costumeHeight);
+        void calculatePos();
+        void calculateRotation();
+        void calculateSize();
+        void handleSceneMouseMove(qreal x, qreal y);
 
         libscratchcpp::IEngine *m_engine = nullptr;
         libscratchcpp::Costume *m_costume = nullptr;
         StageModel *m_stageModel = nullptr;
         SpriteModel *m_spriteModel = nullptr;
+        SceneMouseArea *m_mouseArea = nullptr;
         QSvgRenderer m_svgRenderer;
+        QImage m_costumeBitmap;
         QBuffer m_bitmapBuffer;
         QString m_bitmapUniqueKey;
         QMutex m_costumeMutex;
         QMutex mutex;
-        bool m_imageChanged = false;
-        bool m_visible = true;
+        double m_size = 1;
+        double m_clampedSize = 1;
+        double m_maxSize = 1;
+        unsigned int m_costumeWidth = 0;
+        unsigned int m_costumeHeight = 0;
         double m_x = 0;
         double m_y = 0;
-        double m_z = 0;
-        double m_rotation = 0;
+        double m_width = 0;
+        double m_height = 0;
+        double m_direction = 90;
+        libscratchcpp::Sprite::RotationStyle m_rotationStyle = libscratchcpp::Sprite::RotationStyle::AllAround;
         bool m_mirrorHorizontally = false;
-        bool m_newMirrorHorizontally = false;
-        double m_originX = 0;
-        double m_originY = 0;
+        double m_stageScale = 1;
+        qreal m_maximumWidth = std::numeric_limits<double>::infinity();
+        qreal m_maximumHeight = std::numeric_limits<double>::infinity();
+        std::vector<QPointF> m_hullPoints;
+        bool m_clicked = false; // left mouse button only!
+        double m_dragDeltaX = 0;
+        double m_dragDeltaY = 0;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/src/scenemousearea.cpp b/src/scenemousearea.cpp
new file mode 100644
index 0000000..1632695
--- /dev/null
+++ b/src/scenemousearea.cpp
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <scratchcpp/sprite.h>
+
+#include "scenemousearea.h"
+#include "mouseeventhandler.h"
+
+using namespace scratchcpprender;
+
+SceneMouseArea::SceneMouseArea(QQuickItem *parent) :
+    QQuickItem(parent)
+{
+    m_mouseHandler = new MouseEventHandler(this);
+    installEventFilter(m_mouseHandler);
+    connect(m_mouseHandler, &MouseEventHandler::mouseMoved, this, &SceneMouseArea::mouseMoved);
+    connect(m_mouseHandler, &MouseEventHandler::mousePressed, this, &SceneMouseArea::mousePressed);
+    connect(m_mouseHandler, &MouseEventHandler::mouseReleased, this, &SceneMouseArea::mouseReleased);
+
+    setAcceptHoverEvents(true);
+    setAcceptTouchEvents(true);
+    setAcceptedMouseButtons(Qt::AllButtons);
+}
+
+IRenderedTarget *SceneMouseArea::stage() const
+{
+    return m_mouseHandler->stage();
+}
+
+void SceneMouseArea::setStage(IRenderedTarget *newStage)
+{
+    m_mouseHandler->setStage(newStage);
+    emit stageChanged();
+}
+
+IRenderedTarget *SceneMouseArea::draggedSprite() const
+{
+    return m_draggedSprite;
+}
+
+void SceneMouseArea::setDraggedSprite(IRenderedTarget *sprite)
+{
+    m_draggedSprite = sprite;
+}
+
+ProjectLoader *SceneMouseArea::projectLoader() const
+{
+    return m_mouseHandler->projectLoader();
+}
+
+void SceneMouseArea::setProjectLoader(ProjectLoader *newProjectLoader)
+{
+    m_mouseHandler->setProjectLoader(newProjectLoader);
+    emit projectLoaderChanged();
+}
diff --git a/src/scenemousearea.h b/src/scenemousearea.h
new file mode 100644
index 0000000..e9e2262
--- /dev/null
+++ b/src/scenemousearea.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#pragma once
+
+#include <QQuickItem>
+
+namespace scratchcpprender
+{
+
+class IRenderedTarget;
+class ProjectLoader;
+class MouseEventHandler;
+
+class SceneMouseArea : public QQuickItem
+{
+        Q_OBJECT
+        QML_ELEMENT
+        Q_PROPERTY(IRenderedTarget *stage WRITE setStage NOTIFY stageChanged)
+        Q_PROPERTY(ProjectLoader *projectLoader READ projectLoader WRITE setProjectLoader NOTIFY projectLoaderChanged)
+
+    public:
+        explicit SceneMouseArea(QQuickItem *parent = nullptr);
+
+        IRenderedTarget *stage() const;
+        void setStage(IRenderedTarget *newStage);
+
+        IRenderedTarget *draggedSprite() const;
+        void setDraggedSprite(IRenderedTarget *sprite);
+
+        ProjectLoader *projectLoader() const;
+        void setProjectLoader(ProjectLoader *newProjectLoader);
+
+    signals:
+        void mouseMoved(qreal x, qreal y);
+        void mousePressed();
+        void mouseReleased();
+        void stageChanged();
+        void projectLoaderChanged();
+
+    private:
+        MouseEventHandler *m_mouseHandler = nullptr;
+        IRenderedTarget *m_draggedSprite = nullptr;
+};
+
+} // namespace scratchcpprender
diff --git a/ScratchCPPGui/spritemodel.cpp b/src/spritemodel.cpp
similarity index 50%
rename from ScratchCPPGui/spritemodel.cpp
rename to src/spritemodel.cpp
index 619df17..fbd5cca 100644
--- a/ScratchCPPGui/spritemodel.cpp
+++ b/src/spritemodel.cpp
@@ -1,11 +1,12 @@
 // SPDX-License-Identifier: LGPL-3.0-or-later
 
 #include <scratchcpp/costume.h>
+#include <scratchcpp/rect.h>
 
 #include "spritemodel.h"
 #include "renderedtarget.h"
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 SpriteModel::SpriteModel(QObject *parent) :
@@ -16,13 +17,22 @@ SpriteModel::SpriteModel(QObject *parent) :
 void SpriteModel::init(libscratchcpp::Sprite *sprite)
 {
     m_sprite = sprite;
+}
 
-    if (m_renderedTarget && sprite)
-        m_renderedTarget->loadCostume(sprite->currentCostume().get());
+void SpriteModel::deinitClone()
+{
+    emit cloneDeleted(this);
 }
 
 void SpriteModel::onCloned(libscratchcpp::Sprite *clone)
 {
+    if (!m_cloneRoot)
+        m_cloneRoot = this;
+
+    SpriteModel *cloneModel = new SpriteModel(m_cloneRoot);
+    cloneModel->m_cloneRoot = m_cloneRoot;
+    clone->setInterface(cloneModel);
+    emit cloned(cloneModel);
 }
 
 void SpriteModel::onCostumeChanged(libscratchcpp::Costume *costume)
@@ -33,28 +43,57 @@ void SpriteModel::onCostumeChanged(libscratchcpp::Costume *costume)
 
 void SpriteModel::onVisibleChanged(bool visible)
 {
+    if (m_renderedTarget)
+        m_renderedTarget->updateVisibility(visible);
 }
 
 void SpriteModel::onXChanged(double x)
 {
+    if (m_renderedTarget)
+        m_renderedTarget->updateX(x);
 }
 
 void SpriteModel::onYChanged(double y)
 {
+    if (m_renderedTarget)
+        m_renderedTarget->updateY(y);
 }
 
 void SpriteModel::onSizeChanged(double size)
 {
     if (m_renderedTarget)
-        m_renderedTarget->loadCostume(m_sprite->currentCostume().get());
+        m_renderedTarget->updateSize(size);
 }
 
 void SpriteModel::onDirectionChanged(double direction)
 {
+    if (m_renderedTarget)
+        m_renderedTarget->updateDirection(direction);
 }
 
 void SpriteModel::onRotationStyleChanged(libscratchcpp::Sprite::RotationStyle rotationStyle)
 {
+    if (m_renderedTarget)
+        m_renderedTarget->updateRotationStyle(rotationStyle);
+}
+
+void SpriteModel::onLayerOrderChanged(int layerOrder)
+{
+    if (m_renderedTarget)
+        m_renderedTarget->updateLayerOrder(layerOrder);
+}
+
+void SpriteModel::onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value)
+{
+}
+
+void SpriteModel::onGraphicsEffectsCleared()
+{
+}
+
+libscratchcpp::Rect SpriteModel::boundingRect() const
+{
+    return libscratchcpp::Rect(m_sprite->x(), m_sprite->y(), m_sprite->x(), m_sprite->y());
 }
 
 libscratchcpp::Sprite *SpriteModel::sprite() const
@@ -73,11 +112,7 @@ void SpriteModel::setRenderedTarget(IRenderedTarget *newRenderedTarget)
         return;
 
     m_renderedTarget = newRenderedTarget;
-
-    if (m_renderedTarget && m_sprite)
-        m_renderedTarget->loadCostume(m_sprite->currentCostume().get());
-
     emit renderedTargetChanged();
 }
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/ScratchCPPGui/spritemodel.h b/src/spritemodel.h
similarity index 73%
rename from ScratchCPPGui/spritemodel.h
rename to src/spritemodel.h
index 3f1b759..b7bb5b7 100644
--- a/ScratchCPPGui/spritemodel.h
+++ b/src/spritemodel.h
@@ -8,7 +8,7 @@
 
 Q_MOC_INCLUDE("renderedtarget.h");
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class IRenderedTarget;
@@ -25,6 +25,7 @@ class SpriteModel
         SpriteModel(QObject *parent = nullptr);
 
         void init(libscratchcpp::Sprite *sprite) override;
+        void deinitClone() override;
 
         void onCloned(libscratchcpp::Sprite *clone) override;
 
@@ -36,6 +37,12 @@ class SpriteModel
         void onSizeChanged(double size) override;
         void onDirectionChanged(double direction) override;
         void onRotationStyleChanged(libscratchcpp::Sprite::RotationStyle rotationStyle) override;
+        void onLayerOrderChanged(int layerOrder) override;
+
+        void onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value) override;
+        void onGraphicsEffectsCleared() override;
+
+        libscratchcpp::Rect boundingRect() const override;
 
         libscratchcpp::Sprite *sprite() const;
 
@@ -44,10 +51,13 @@ class SpriteModel
 
     signals:
         void renderedTargetChanged();
+        void cloned(SpriteModel *cloneModel);
+        void cloneDeleted(SpriteModel *clone);
 
     private:
         libscratchcpp::Sprite *m_sprite = nullptr;
         IRenderedTarget *m_renderedTarget = nullptr;
+        SpriteModel *m_cloneRoot = nullptr;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/ScratchCPPGui/stagemodel.cpp b/src/stagemodel.cpp
similarity index 72%
rename from ScratchCPPGui/stagemodel.cpp
rename to src/stagemodel.cpp
index 3128692..14521a9 100644
--- a/ScratchCPPGui/stagemodel.cpp
+++ b/src/stagemodel.cpp
@@ -5,7 +5,7 @@
 #include "stagemodel.h"
 #include "renderedtarget.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 
 StageModel::StageModel(QObject *parent) :
     QObject(parent)
@@ -15,9 +15,6 @@ StageModel::StageModel(QObject *parent) :
 void StageModel::init(libscratchcpp::Stage *stage)
 {
     m_stage = stage;
-
-    if (m_renderedTarget && stage)
-        m_renderedTarget->loadCostume(stage->currentCostume().get());
 }
 
 void StageModel::onCostumeChanged(libscratchcpp::Costume *costume)
@@ -38,6 +35,22 @@ void StageModel::onVideoTransparencyChanged(int videoTransparency)
 {
 }
 
+void StageModel::onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value)
+{
+}
+
+void StageModel::onGraphicsEffectsCleared()
+{
+}
+
+void StageModel::loadCostume()
+{
+    if (m_renderedTarget && m_stage) {
+        if (m_stage)
+            m_renderedTarget->loadCostume(m_stage->currentCostume().get());
+    }
+}
+
 libscratchcpp::Stage *StageModel::stage() const
 {
     return m_stage;
@@ -54,9 +67,7 @@ void StageModel::setRenderedTarget(IRenderedTarget *newRenderedTarget)
         return;
 
     m_renderedTarget = newRenderedTarget;
-
-    if (m_renderedTarget && m_stage)
-        m_renderedTarget->loadCostume(m_stage->currentCostume().get());
+    loadCostume();
 
     emit renderedTargetChanged();
 }
diff --git a/ScratchCPPGui/stagemodel.h b/src/stagemodel.h
similarity index 82%
rename from ScratchCPPGui/stagemodel.h
rename to src/stagemodel.h
index 71e9e18..51a7066 100644
--- a/ScratchCPPGui/stagemodel.h
+++ b/src/stagemodel.h
@@ -7,7 +7,7 @@
 
 Q_MOC_INCLUDE("renderedtarget.h");
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class IRenderedTarget;
@@ -30,6 +30,11 @@ class StageModel
         void onVideoStateChanged(libscratchcpp::Stage::VideoState videoState) override;
         void onVideoTransparencyChanged(int videoTransparency) override;
 
+        void onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value) override;
+        void onGraphicsEffectsCleared() override;
+
+        Q_INVOKABLE void loadCostume();
+
         libscratchcpp::Stage *stage() const;
 
         IRenderedTarget *renderedTarget() const;
@@ -43,4 +48,4 @@ class StageModel
         IRenderedTarget *m_renderedTarget = nullptr;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp
new file mode 100644
index 0000000..c91c3e9
--- /dev/null
+++ b/src/targetpainter.cpp
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <scratchcpp/target.h>
+#include <scratchcpp/costume.h>
+
+#include "targetpainter.h"
+#include "irenderedtarget.h"
+#include "spritemodel.h"
+
+using namespace scratchcpprender;
+
+TargetPainter::TargetPainter()
+{
+}
+
+TargetPainter::~TargetPainter()
+{
+}
+
+void TargetPainter::paint(QNanoPainter *painter)
+{
+    if (QThread::currentThread() != qApp->thread()) {
+        qFatal("Error: Rendering must happen in the GUI thread to work correctly. Please disable threaded render loop using qputenv(\"QSG_RENDER_LOOP\", \"basic\") before constructing your "
+               "application object.");
+    }
+
+    m_target->lockCostume();
+    double width = m_target->width();
+    double height = m_target->height();
+
+    if (m_target->isSvg())
+        m_target->paintSvg(painter);
+    else {
+        QOpenGLContext *context = QOpenGLContext::currentContext();
+        Q_ASSERT(context);
+
+        if (!context)
+            return;
+
+        QOffscreenSurface surface;
+        surface.setFormat(context->format());
+        surface.create();
+        Q_ASSERT(surface.isValid());
+
+        QSurface *oldSurface = context->surface();
+        context->makeCurrent(&surface);
+
+        painter->beginFrame(width, height);
+        QNanoImage image = QNanoImage::fromCache(painter, m_target->bitmapBuffer(), m_target->bitmapUniqueKey());
+        painter->drawImage(image, 0, 0, width, height);
+        painter->endFrame();
+
+        context->doneCurrent();
+        context->makeCurrent(oldSurface);
+    }
+
+    m_target->updateHullPoints(framebufferObject());
+    m_target->unlockCostume();
+}
+
+void TargetPainter::synchronize(QNanoQuickItem *item)
+{
+    m_target = dynamic_cast<IRenderedTarget *>(item);
+    Q_ASSERT(m_target);
+}
diff --git a/ScratchCPPGui/targetpainter.h b/src/targetpainter.h
similarity index 87%
rename from ScratchCPPGui/targetpainter.h
rename to src/targetpainter.h
index c70b6d8..e752f96 100644
--- a/ScratchCPPGui/targetpainter.h
+++ b/src/targetpainter.h
@@ -4,7 +4,7 @@
 
 #include <qnanoquickitempainter.h>
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class IRenderedTarget;
@@ -22,4 +22,4 @@ class TargetPainter : public QNanoQuickItemPainter
         IRenderedTarget *m_target = nullptr;
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/src/valuemonitormodel.cpp b/src/valuemonitormodel.cpp
new file mode 100644
index 0000000..d716730
--- /dev/null
+++ b/src/valuemonitormodel.cpp
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <scratchcpp/virtualmachine.h>
+#include <scratchcpp/iblocksection.h>
+#include <scratchcpp/monitor.h>
+
+#include "valuemonitormodel.h"
+
+using namespace scratchcpprender;
+using namespace libscratchcpp;
+
+static const std::unordered_map<Monitor::Mode, ValueMonitorModel::Mode>
+    MODE_MAP = { { Monitor::Mode::Default, ValueMonitorModel::Mode::Default }, { Monitor::Mode::Large, ValueMonitorModel::Mode::Large }, { Monitor::Mode::Slider, ValueMonitorModel::Mode::Slider } };
+
+ValueMonitorModel::ValueMonitorModel(QObject *parent) :
+    MonitorModel(parent)
+{
+}
+
+ValueMonitorModel::ValueMonitorModel(IBlockSection *section, QObject *parent) :
+    MonitorModel(parent)
+{
+    if (!section)
+        return;
+
+    // TODO: Get the color from the block section
+    std::string name = section->name();
+    if (name == "Motion")
+        m_color = QColor::fromString("#4C97FF");
+    else if (name == "Looks")
+        m_color = QColor::fromString("#9966FF");
+    else if (name == "Sound")
+        m_color = QColor::fromString("#CF63CF");
+    else if (name == "Sensing")
+        m_color = QColor::fromString("#5CB1D6");
+    else if (name == "Variables")
+        m_color = QColor::fromString("#FF8C1A");
+    else if (name == "Lists")
+        m_color = QColor::fromString("#FF661A");
+}
+
+void ValueMonitorModel::onValueChanged(const VirtualMachine *vm)
+{
+    if (vm->registerCount() == 1) {
+        m_value = QString::fromStdString(vm->getInput(0, 1)->toString());
+        emit valueChanged();
+    }
+}
+
+MonitorModel::Type ValueMonitorModel::type() const
+{
+    return Type::Value;
+}
+
+const QString &ValueMonitorModel::value() const
+{
+    return m_value;
+}
+
+void ValueMonitorModel::setValue(const QString &newValue)
+{
+    if (newValue == m_value)
+        return;
+
+    if (monitor())
+        monitor()->changeValue(newValue.toStdString());
+    else {
+        m_value = newValue;
+        emit valueChanged();
+    }
+}
+
+const QColor &ValueMonitorModel::color() const
+{
+    return m_color;
+}
+
+ValueMonitorModel::Mode ValueMonitorModel::mode() const
+{
+    if (monitor())
+        return MODE_MAP.at(monitor()->mode());
+    else
+        return Mode::Default;
+}
+
+double ValueMonitorModel::sliderMin() const
+{
+    if (monitor())
+        return monitor()->sliderMin();
+    else
+        return 0;
+}
+
+double ValueMonitorModel::sliderMax() const
+{
+    if (monitor())
+        return monitor()->sliderMax();
+    else
+        return 0;
+}
+
+bool ValueMonitorModel::discrete() const
+{
+    if (monitor())
+        return monitor()->discrete();
+    else
+        return true;
+}
diff --git a/src/valuemonitormodel.h b/src/valuemonitormodel.h
new file mode 100644
index 0000000..aa7f2cc
--- /dev/null
+++ b/src/valuemonitormodel.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#pragma once
+
+#include <QColor>
+
+#include "monitormodel.h"
+
+namespace libscratchcpp
+{
+
+class IBlockSection;
+
+}
+
+namespace scratchcpprender
+{
+
+class ValueMonitorModel : public MonitorModel
+{
+        Q_OBJECT
+        QML_ELEMENT
+        Q_PROPERTY(QString value READ value WRITE setValue NOTIFY valueChanged)
+        Q_PROPERTY(QColor color READ color NOTIFY colorChanged)
+        Q_PROPERTY(Mode mode READ mode NOTIFY modeChanged)
+        Q_PROPERTY(double sliderMin READ sliderMin NOTIFY sliderMinChanged)
+        Q_PROPERTY(double sliderMax READ sliderMax NOTIFY sliderMaxChanged)
+        Q_PROPERTY(bool discrete READ discrete NOTIFY discreteChanged)
+
+    public:
+        enum class Mode
+        {
+            Default,
+            Large,
+            Slider
+        };
+
+        Q_ENUM(Mode)
+
+        ValueMonitorModel(QObject *parent = nullptr);
+        ValueMonitorModel(libscratchcpp::IBlockSection *section, QObject *parent = nullptr);
+
+        void onValueChanged(const libscratchcpp::VirtualMachine *vm) override;
+
+        Type type() const override;
+
+        const QString &value() const;
+        void setValue(const QString &newValue);
+
+        const QColor &color() const;
+        Mode mode() const;
+        double sliderMin() const;
+        double sliderMax() const;
+        bool discrete() const;
+
+    signals:
+        void valueChanged();
+        void colorChanged();
+        void modeChanged();
+        void sliderMinChanged();
+        void sliderMaxChanged();
+        void discreteChanged();
+
+    private:
+        QString m_value;
+        QColor m_color = Qt::green;
+};
+
+} // namespace scratchcpprender
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 85bfe9e..20be59f 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.14)
-project(libscratchcpp_test)
+project(scratchcpp-render_test)
 
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -14,7 +14,7 @@ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
 include(GoogleTest)
 
 function(add_test TARGET)
-    target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/ScratchCPPGui)
+    target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/src)
     target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/thirdparty/libqnanopainter)
 endfunction()
 
@@ -25,3 +25,7 @@ add_subdirectory(renderedtarget)
 add_subdirectory(targetpainter)
 add_subdirectory(target_models)
 add_subdirectory(projectscene)
+add_subdirectory(keyeventhandler)
+add_subdirectory(mouseeventhandler)
+add_subdirectory(scenemousearea)
+add_subdirectory(monitor_models)
diff --git a/test/clones.sb3 b/test/clones.sb3
new file mode 100644
index 0000000..315ed3f
Binary files /dev/null and b/test/clones.sb3 differ
diff --git a/test/keyeventhandler/CMakeLists.txt b/test/keyeventhandler/CMakeLists.txt
new file mode 100644
index 0000000..38a872f
--- /dev/null
+++ b/test/keyeventhandler/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_executable(
+  keyeventhandler_test
+  keyeventhandler_test.cpp
+)
+
+target_link_libraries(
+  keyeventhandler_test
+  GTest::gtest_main
+  scratchcpp-render
+  ${QT_LIBS}
+  Qt6::Test
+)
+
+add_test(keyeventhandler_test)
+gtest_discover_tests(keyeventhandler_test)
diff --git a/test/keyeventhandler/keyeventhandler_test.cpp b/test/keyeventhandler/keyeventhandler_test.cpp
new file mode 100644
index 0000000..b5bcb0d
--- /dev/null
+++ b/test/keyeventhandler/keyeventhandler_test.cpp
@@ -0,0 +1,32 @@
+#include <QtTest/QSignalSpy>
+#include <QKeyEvent>
+#include <keyeventhandler.h>
+
+#include "../common.h"
+
+using namespace scratchcpprender;
+
+TEST(KeyEventHandlerTest, EventFilter)
+{
+    KeyEventHandler handler;
+    QSignalSpy keyPressedSpy(&handler, &KeyEventHandler::keyPressed);
+    QSignalSpy keyReleasedSpy(&handler, &KeyEventHandler::keyReleased);
+
+    {
+        QKeyEvent event(QEvent::KeyPress, Qt::Key_Space, Qt::ShiftModifier | Qt::AltModifier, " ", true, 1);
+        handler.eventFilter(nullptr, &event);
+        ASSERT_EQ(keyPressedSpy.count(), 1);
+        ASSERT_EQ(keyReleasedSpy.count(), 0);
+        keyPressedSpy.clear();
+        keyReleasedSpy.clear();
+    }
+
+    {
+        QKeyEvent event(QEvent::KeyRelease, Qt::Key_Space, Qt::ShiftModifier | Qt::AltModifier, " ", true, 1);
+        handler.eventFilter(nullptr, &event);
+        ASSERT_EQ(keyPressedSpy.count(), 0);
+        ASSERT_EQ(keyReleasedSpy.count(), 1);
+        keyPressedSpy.clear();
+        keyReleasedSpy.clear();
+    }
+}
diff --git a/test/load_test.sb3 b/test/load_test.sb3
index cd7c8eb..d1553e8 100644
Binary files a/test/load_test.sb3 and b/test/load_test.sb3 differ
diff --git a/test/mocks/CMakeLists.txt b/test/mocks/CMakeLists.txt
index e5fbb72..d61a18a 100644
--- a/test/mocks/CMakeLists.txt
+++ b/test/mocks/CMakeLists.txt
@@ -1,2 +1,2 @@
-add_library(scratchcppgui_mocks INTERFACE)
-target_include_directories(scratchcppgui_mocks INTERFACE .)
+add_library(scratchcpprender_mocks INTERFACE)
+target_include_directories(scratchcpprender_mocks INTERFACE .)
diff --git a/test/mocks/blocksectionmock.h b/test/mocks/blocksectionmock.h
new file mode 100644
index 0000000..1963f11
--- /dev/null
+++ b/test/mocks/blocksectionmock.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <scratchcpp/iblocksection.h>
+#include <gmock/gmock.h>
+
+using namespace libscratchcpp;
+
+namespace scratchcpprender
+{
+
+class BlockSectionMock : public IBlockSection
+{
+    public:
+        MOCK_METHOD(std::string, name, (), (const, override));
+        MOCK_METHOD(bool, categoryVisible, (), (const, override));
+        MOCK_METHOD(void, registerBlocks, (IEngine * engine), (override));
+};
+
+} // namespace scratchcpprender
diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h
index 805e52c..6d5f988 100644
--- a/test/mocks/enginemock.h
+++ b/test/mocks/enginemock.h
@@ -5,7 +5,7 @@
 
 using namespace libscratchcpp;
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 using ScriptMap = std::unordered_map<std::shared_ptr<Block>, std::shared_ptr<Script>>;
@@ -18,14 +18,18 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(void, start, (), (override));
         MOCK_METHOD(void, stop, (), (override));
-        MOCK_METHOD(void, startScript, (std::shared_ptr<Block>, std::shared_ptr<Target>), (override));
-        MOCK_METHOD(void, broadcast, (unsigned int, VirtualMachine *, bool), (override));
-        MOCK_METHOD(void, broadcastByPtr, (Broadcast *, VirtualMachine *, bool), (override));
+        MOCK_METHOD(VirtualMachine *, startScript, (std::shared_ptr<Block>, Target *), (override));
+        MOCK_METHOD(void, broadcast, (unsigned int), (override));
+        MOCK_METHOD(void, broadcastByPtr, (Broadcast *), (override));
+        MOCK_METHOD(void, startBackdropScripts, (Broadcast *), (override));
         MOCK_METHOD(void, stopScript, (VirtualMachine *), (override));
         MOCK_METHOD(void, stopTarget, (Target *, VirtualMachine *), (override));
-        MOCK_METHOD(void, initClone, (Sprite *), (override));
-        MOCK_METHOD(void, deinitClone, (Sprite *), (override));
+        MOCK_METHOD(void, initClone, (std::shared_ptr<Sprite>), (override));
+        MOCK_METHOD(void, deinitClone, (std::shared_ptr<Sprite>), (override));
+        MOCK_METHOD(void, stopSounds, (), (override));
 
+        MOCK_METHOD(void, updateMonitors, (), (override));
+        MOCK_METHOD(void, step, (), (override));
         MOCK_METHOD(void, run, (), (override));
         MOCK_METHOD(void, runEventLoop, (), (override));
         MOCK_METHOD(void, stopEventLoop, (), (override));
@@ -42,6 +46,7 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(bool, keyPressed, (const std::string &), (const, override));
         MOCK_METHOD(void, setKeyState, (const std::string &, bool), (override));
+        MOCK_METHOD(void, setKeyState, (const KeyEvent &, bool), (override));
         MOCK_METHOD(void, setAnyKeyPressed, (bool), (override));
 
         MOCK_METHOD(double, mouseX, (), (const, override));
@@ -53,6 +58,8 @@ class EngineMock : public IEngine
         MOCK_METHOD(bool, mousePressed, (), (const, override));
         MOCK_METHOD(void, setMousePressed, (bool), (override));
 
+        MOCK_METHOD(void, clickTarget, (Target * target), (override));
+
         MOCK_METHOD(unsigned int, stageWidth, (), (const, override));
         MOCK_METHOD(void, setStageWidth, (unsigned int), (override));
 
@@ -67,8 +74,8 @@ class EngineMock : public IEngine
         MOCK_METHOD(bool, spriteFencingEnabled, (), (const, override));
         MOCK_METHOD(void, setSpriteFencingEnabled, (bool), (override));
 
-        MOCK_METHOD(bool, broadcastRunning, (unsigned int, VirtualMachine *), (override));
-        MOCK_METHOD(bool, broadcastByPtrRunning, (Broadcast *, VirtualMachine *), (override));
+        MOCK_METHOD(bool, broadcastRunning, (unsigned int), (override));
+        MOCK_METHOD(bool, broadcastByPtrRunning, (Broadcast *), (override));
 
         MOCK_METHOD(void, requestRedraw, (), (override));
 
@@ -78,6 +85,8 @@ class EngineMock : public IEngine
         MOCK_METHOD(unsigned int, functionIndex, (BlockFunc), (override));
 
         MOCK_METHOD(void, addCompileFunction, (IBlockSection *, const std::string &, BlockComp), (override));
+        MOCK_METHOD(void, addMonitorNameFunction, (IBlockSection *, const std::string &, MonitorNameFunc), (override));
+        MOCK_METHOD(void, addMonitorChangeFunction, (IBlockSection *, const std::string &, MonitorChangeFunc), (override));
         MOCK_METHOD(void, addHatBlock, (IBlockSection *, const std::string &), (override));
         MOCK_METHOD(void, addInput, (IBlockSection *, const std::string &, int), (override));
         MOCK_METHOD(void, addField, (IBlockSection *, const std::string &, int), (override));
@@ -89,9 +98,11 @@ class EngineMock : public IEngine
         MOCK_METHOD(int, findBroadcast, (const std::string &), (const, override));
         MOCK_METHOD(int, findBroadcastById, (const std::string &), (const, override));
 
-        MOCK_METHOD(void, addBroadcastScript, (std::shared_ptr<Block>, Broadcast *), (override));
+        MOCK_METHOD(void, addGreenFlagScript, (std::shared_ptr<Block>), (override));
+        MOCK_METHOD(void, addBroadcastScript, (std::shared_ptr<Block>, int, Broadcast *), (override));
+        MOCK_METHOD(void, addBackdropChangeScript, (std::shared_ptr<Block>, int), (override));
         MOCK_METHOD(void, addCloneInitScript, (std::shared_ptr<Block>), (override));
-        MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr<Block>, std::string), (override));
+        MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr<Block>, int), (override));
 
         MOCK_METHOD(const std::vector<std::shared_ptr<Target>> &, targets, (), (const, override));
         MOCK_METHOD(void, setTargets, (const std::vector<std::shared_ptr<Target>> &), (override));
@@ -106,10 +117,15 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(Stage *, stage, (), (const, override));
 
+        MOCK_METHOD(const std::vector<std::shared_ptr<Monitor>> &, monitors, (), (const, override));
+        MOCK_METHOD(void, setMonitors, (const std::vector<std::shared_ptr<Monitor>> &), (override));
+        MOCK_METHOD(void, setAddMonitorHandler, (const std::function<void(Monitor *)> &), (override));
+        MOCK_METHOD(void, setRemoveMonitorHandler, (const std::function<void(Monitor *, IMonitorHandler *)> &), (override));
+
         MOCK_METHOD(std::vector<std::string> &, extensions, (), (const, override));
         MOCK_METHOD(void, setExtensions, (const std::vector<std::string> &), (override));
 
         MOCK_METHOD(const ScriptMap &, scripts, (), (const, override));
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h
index 4010bb0..ab7cf2c 100644
--- a/test/mocks/renderedtargetmock.h
+++ b/test/mocks/renderedtargetmock.h
@@ -4,17 +4,27 @@
 #include <qnanoquickitem.h>
 #include <gmock/gmock.h>
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 
-namespace scratchcppgui
+namespace scratchcpprender
 {
 
 class RenderedTargetMock : public IRenderedTarget
 {
     public:
-        MOCK_METHOD(void, loadProperties, (), (override));
+        MOCK_METHOD(void, updateVisibility, (bool), (override));
+        MOCK_METHOD(void, updateX, (double), (override));
+        MOCK_METHOD(void, updateY, (double), (override));
+        MOCK_METHOD(void, updateSize, (double), (override));
+        MOCK_METHOD(void, updateDirection, (double), (override));
+        MOCK_METHOD(void, updateRotationStyle, (libscratchcpp::Sprite::RotationStyle), (override));
+        MOCK_METHOD(void, updateLayerOrder, (int), (override));
+
+        MOCK_METHOD(void, beforeRedraw, (), (override));
+
         MOCK_METHOD(void, loadCostume, (libscratchcpp::Costume *), (override));
-        MOCK_METHOD(void, updateProperties, (), (override));
+
+        MOCK_METHOD(void, deinitClone, (), (override));
 
         MOCK_METHOD(libscratchcpp::IEngine *, engine, (), (const, override));
         MOCK_METHOD(void, setEngine, (libscratchcpp::IEngine *), (override));
@@ -27,12 +37,20 @@ class RenderedTargetMock : public IRenderedTarget
 
         MOCK_METHOD(libscratchcpp::Target *, scratchTarget, (), (const, override));
 
+        MOCK_METHOD(SceneMouseArea *, mouseArea, (), (const, override));
+        MOCK_METHOD(void, setMouseArea, (SceneMouseArea *), (override));
+
+        MOCK_METHOD(double, stageScale, (), (const, override));
+        MOCK_METHOD(void, setStageScale, (double), (override));
+
         MOCK_METHOD(qreal, width, (), (const, override));
         MOCK_METHOD(void, setWidth, (qreal), (override));
 
         MOCK_METHOD(qreal, height, (), (const, override));
         MOCK_METHOD(void, setHeight, (qreal), (override));
 
+        MOCK_METHOD(QPointF, mapFromScene, (const QPointF &), (const, override));
+
         MOCK_METHOD(QBuffer *, bitmapBuffer, (), (override));
         MOCK_METHOD(const QString &, bitmapUniqueKey, (), (const, override));
 
@@ -44,7 +62,17 @@ class RenderedTargetMock : public IRenderedTarget
         MOCK_METHOD(bool, isSvg, (), (const, override));
         MOCK_METHOD(void, paintSvg, (QNanoPainter *), (override));
 
+        MOCK_METHOD(void, updateHullPoints, (QOpenGLFramebufferObject *), (override));
+        MOCK_METHOD(const std::vector<QPointF> &, hullPoints, (), (const, override));
+
+        MOCK_METHOD(bool, contains, (const QPointF &), (const, override));
         MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override));
+        MOCK_METHOD(void, hoverEnterEvent, (QHoverEvent *), (override));
+        MOCK_METHOD(void, hoverLeaveEvent, (QHoverEvent *), (override));
+        MOCK_METHOD(void, hoverMoveEvent, (QHoverEvent *), (override));
+        MOCK_METHOD(void, mouseMoveEvent, (QMouseEvent *), (override));
+        MOCK_METHOD(void, mousePressEvent, (QMouseEvent *), (override));
+        MOCK_METHOD(void, mouseReleaseEvent, (QMouseEvent *), (override));
 };
 
-} // namespace scratchcppgui
+} // namespace scratchcpprender
diff --git a/test/monitor_models/CMakeLists.txt b/test/monitor_models/CMakeLists.txt
new file mode 100644
index 0000000..ebd6d03
--- /dev/null
+++ b/test/monitor_models/CMakeLists.txt
@@ -0,0 +1,71 @@
+# monitormodel
+add_executable(
+  monitormodel_test
+  monitormodel_test.cpp
+)
+
+target_link_libraries(
+  monitormodel_test
+  GTest::gtest_main
+  scratchcpp-render
+  ${QT_LIBS}
+  Qt6::Test
+)
+
+add_test(monitormodel_test)
+gtest_discover_tests(monitormodel_test)
+
+# valuemonitormodel
+add_executable(
+  valuemonitormodel_test
+  valuemonitormodel_test.cpp
+)
+
+target_link_libraries(
+  valuemonitormodel_test
+  GTest::gtest_main
+  GTest::gmock_main
+  scratchcpp-render
+  scratchcpprender_mocks
+  ${QT_LIBS}
+  Qt6::Test
+)
+
+add_test(valuemonitormodel_test)
+gtest_discover_tests(valuemonitormodel_test)
+
+# listmonitorlistmodel
+add_executable(
+  listmonitorlistmodel_test
+  listmonitorlistmodel_test.cpp
+)
+
+target_link_libraries(
+  listmonitorlistmodel_test
+  GTest::gtest_main
+  scratchcpp-render
+  ${QT_LIBS}
+  Qt6::Test
+)
+
+add_test(listmonitorlistmodel_test)
+gtest_discover_tests(listmonitorlistmodel_test)
+
+# listmonitormodel
+add_executable(
+  listmonitormodel_test
+  listmonitormodel_test.cpp
+)
+
+target_link_libraries(
+  listmonitormodel_test
+  GTest::gtest_main
+  GTest::gmock_main
+  scratchcpp-render
+  scratchcpprender_mocks
+  ${QT_LIBS}
+  Qt6::Test
+)
+
+add_test(listmonitormodel_test)
+gtest_discover_tests(listmonitormodel_test)
diff --git a/test/monitor_models/listmonitorlistmodel_test.cpp b/test/monitor_models/listmonitorlistmodel_test.cpp
new file mode 100644
index 0000000..91584b3
--- /dev/null
+++ b/test/monitor_models/listmonitorlistmodel_test.cpp
@@ -0,0 +1,218 @@
+#include <QtTest/QSignalSpy>
+#include <scratchcpp/list.h>
+#include <listmonitorlistmodel.h>
+
+#include "../common.h"
+
+using namespace scratchcpprender;
+using namespace libscratchcpp;
+
+TEST(ListMonitorListModelTest, Constructors)
+{
+    ListMonitorListModel model1;
+    ListMonitorListModel model2(&model1);
+    ASSERT_EQ(model2.parent(), &model1);
+}
+
+TEST(ListMonitorListModelTest, LoadData)
+{
+    ListMonitorListModel model;
+    QSignalSpy dataChangedSpy(&model, &ListMonitorListModel::dataChanged);
+    QSignalSpy aboutToResetSpy(&model, &ListMonitorListModel::modelAboutToBeReset);
+    QSignalSpy resetSpy(&model, &ListMonitorListModel::modelReset);
+    QSignalSpy aboutToInsertSpy(&model, &ListMonitorListModel::rowsAboutToBeInserted);
+    QSignalSpy insertSpy(&model, &ListMonitorListModel::rowsInserted);
+    QSignalSpy aboutToRemoveSpy(&model, &ListMonitorListModel::rowsAboutToBeRemoved);
+    QSignalSpy removeSpy(&model, &ListMonitorListModel::rowsRemoved);
+
+    List list1("", "");
+    list1.push_back(1);
+    list1.push_back(2);
+    model.setList(&list1);
+    ASSERT_TRUE(dataChangedSpy.empty());
+    ASSERT_EQ(aboutToResetSpy.count(), 1);
+    ASSERT_EQ(resetSpy.count(), 1);
+    ASSERT_TRUE(aboutToInsertSpy.empty());
+    ASSERT_TRUE(insertSpy.empty());
+    ASSERT_TRUE(aboutToRemoveSpy.empty());
+    ASSERT_TRUE(removeSpy.empty());
+
+    model.setList(&list1);
+    ASSERT_EQ(dataChangedSpy.count(), 2);
+    ASSERT_EQ(aboutToResetSpy.count(), 1);
+    ASSERT_EQ(resetSpy.count(), 1);
+    ASSERT_TRUE(aboutToInsertSpy.empty());
+    ASSERT_TRUE(insertSpy.empty());
+    ASSERT_TRUE(aboutToRemoveSpy.empty());
+    ASSERT_TRUE(removeSpy.empty());
+
+    auto args = dataChangedSpy.at(0);
+    QModelIndex arg1 = args.at(0).value<QModelIndex>();
+    QModelIndex arg2 = args.at(1).value<QModelIndex>();
+    ASSERT_EQ(arg1.row(), 0);
+    ASSERT_EQ(arg1.column(), 0);
+    ASSERT_EQ(arg2.row(), 0);
+    ASSERT_EQ(arg2.column(), 0);
+    ASSERT_TRUE(args.at(2).toList().isEmpty());
+
+    args = dataChangedSpy.at(1);
+    arg1 = args.at(0).value<QModelIndex>();
+    arg2 = args.at(1).value<QModelIndex>();
+    ASSERT_EQ(arg1.row(), 1);
+    ASSERT_EQ(arg1.column(), 0);
+    ASSERT_EQ(arg2.row(), 1);
+    ASSERT_EQ(arg2.column(), 0);
+    ASSERT_TRUE(args.at(2).toList().isEmpty());
+
+    List list2("", "");
+    model.setList(&list2);
+    ASSERT_EQ(dataChangedSpy.count(), 2);
+    ASSERT_EQ(aboutToResetSpy.count(), 2);
+    ASSERT_EQ(resetSpy.count(), 2);
+    ASSERT_TRUE(aboutToInsertSpy.empty());
+    ASSERT_TRUE(insertSpy.empty());
+    ASSERT_TRUE(aboutToRemoveSpy.empty());
+    ASSERT_TRUE(removeSpy.empty());
+}
+
+TEST(ListMonitorListModelTest, AddRows)
+{
+    ListMonitorListModel model;
+    QSignalSpy dataChangedSpy(&model, &ListMonitorListModel::dataChanged);
+    QSignalSpy aboutToResetSpy(&model, &ListMonitorListModel::modelAboutToBeReset);
+    QSignalSpy resetSpy(&model, &ListMonitorListModel::modelReset);
+    QSignalSpy aboutToInsertSpy(&model, &ListMonitorListModel::rowsAboutToBeInserted);
+    QSignalSpy insertSpy(&model, &ListMonitorListModel::rowsInserted);
+    QSignalSpy aboutToRemoveSpy(&model, &ListMonitorListModel::rowsAboutToBeRemoved);
+    QSignalSpy removeSpy(&model, &ListMonitorListModel::rowsRemoved);
+
+    List list1("", "");
+    list1.push_back(1);
+    list1.push_back(2);
+    model.setList(&list1);
+    ASSERT_TRUE(dataChangedSpy.empty());
+    ASSERT_EQ(aboutToResetSpy.count(), 1);
+    ASSERT_EQ(resetSpy.count(), 1);
+    ASSERT_TRUE(aboutToInsertSpy.empty());
+    ASSERT_TRUE(insertSpy.empty());
+    ASSERT_TRUE(aboutToRemoveSpy.empty());
+    ASSERT_TRUE(removeSpy.empty());
+
+    list1.push_back(9);
+    list1.push_back(8);
+    list1.push_back(7);
+    model.setList(&list1);
+    ASSERT_EQ(dataChangedSpy.count(), 2);
+    ASSERT_EQ(aboutToResetSpy.count(), 1);
+    ASSERT_EQ(resetSpy.count(), 1);
+    ASSERT_EQ(aboutToInsertSpy.count(), 1);
+    ASSERT_EQ(insertSpy.count(), 1);
+    ASSERT_TRUE(aboutToRemoveSpy.empty());
+    ASSERT_TRUE(removeSpy.empty());
+
+    auto args = dataChangedSpy.at(0);
+    QModelIndex arg1 = args.at(0).value<QModelIndex>();
+    QModelIndex arg2 = args.at(1).value<QModelIndex>();
+    ASSERT_EQ(arg1.row(), 0);
+    ASSERT_EQ(arg1.column(), 0);
+    ASSERT_EQ(arg2.row(), 0);
+    ASSERT_EQ(arg2.column(), 0);
+    ASSERT_TRUE(args.at(2).toList().isEmpty());
+
+    args = dataChangedSpy.at(1);
+    arg1 = args.at(0).value<QModelIndex>();
+    arg2 = args.at(1).value<QModelIndex>();
+    ASSERT_EQ(arg1.row(), 1);
+    ASSERT_EQ(arg1.column(), 0);
+    ASSERT_EQ(arg2.row(), 1);
+    ASSERT_EQ(arg2.column(), 0);
+    ASSERT_TRUE(args.at(2).toList().isEmpty());
+
+    args = aboutToInsertSpy.at(0);
+    arg1 = args.at(0).value<QModelIndex>();
+    ASSERT_EQ(arg1, QModelIndex());
+    ASSERT_EQ(args.at(1).toInt(), 2);
+    ASSERT_EQ(args.at(2).toInt(), 4);
+}
+
+TEST(ListMonitorListModelTest, RemoveRows)
+{
+    ListMonitorListModel model;
+    QSignalSpy dataChangedSpy(&model, &ListMonitorListModel::dataChanged);
+    QSignalSpy aboutToResetSpy(&model, &ListMonitorListModel::modelAboutToBeReset);
+    QSignalSpy resetSpy(&model, &ListMonitorListModel::modelReset);
+    QSignalSpy aboutToInsertSpy(&model, &ListMonitorListModel::rowsAboutToBeInserted);
+    QSignalSpy insertSpy(&model, &ListMonitorListModel::rowsInserted);
+    QSignalSpy aboutToRemoveSpy(&model, &ListMonitorListModel::rowsAboutToBeRemoved);
+    QSignalSpy removeSpy(&model, &ListMonitorListModel::rowsRemoved);
+
+    List list1("", "");
+    list1.push_back(1);
+    list1.push_back(2);
+    list1.push_back(3);
+    model.setList(&list1);
+    ASSERT_TRUE(dataChangedSpy.empty());
+    ASSERT_EQ(aboutToResetSpy.count(), 1);
+    ASSERT_EQ(resetSpy.count(), 1);
+    ASSERT_TRUE(aboutToInsertSpy.empty());
+    ASSERT_TRUE(insertSpy.empty());
+    ASSERT_TRUE(aboutToRemoveSpy.empty());
+    ASSERT_TRUE(removeSpy.empty());
+
+    list1.removeAt(2);
+    list1.removeAt(0);
+    model.setList(&list1);
+    ASSERT_EQ(dataChangedSpy.count(), 1);
+    ASSERT_EQ(aboutToResetSpy.count(), 1);
+    ASSERT_EQ(resetSpy.count(), 1);
+    ASSERT_TRUE(aboutToInsertSpy.empty());
+    ASSERT_TRUE(insertSpy.empty());
+    ASSERT_EQ(aboutToRemoveSpy.count(), 1);
+    ASSERT_EQ(removeSpy.count(), 1);
+
+    auto args = dataChangedSpy.at(0);
+    QModelIndex arg1 = args.at(0).value<QModelIndex>();
+    QModelIndex arg2 = args.at(1).value<QModelIndex>();
+    ASSERT_EQ(arg1.row(), 0);
+    ASSERT_EQ(arg1.column(), 0);
+    ASSERT_EQ(arg2.row(), 0);
+    ASSERT_EQ(arg2.column(), 0);
+    ASSERT_TRUE(args.at(2).toList().isEmpty());
+
+    args = aboutToRemoveSpy.at(0);
+    arg1 = args.at(0).value<QModelIndex>();
+    ASSERT_EQ(arg1, QModelIndex());
+    ASSERT_EQ(args.at(1).toInt(), 1);
+    ASSERT_EQ(args.at(2).toInt(), 2);
+}
+
+TEST(ListMonitorListModelTest, RowCount)
+{
+    ListMonitorListModel model;
+    List list("", "");
+    list.push_back(1);
+    list.push_back(2);
+    list.push_back(3);
+    model.setList(&list);
+    ASSERT_EQ(model.rowCount(QModelIndex()), list.size());
+}
+
+TEST(ListMonitorListModelTest, Data)
+{
+    ListMonitorListModel model;
+    List list("", "");
+    list.push_back(1);
+    list.push_back(2);
+    list.push_back(3);
+    model.setList(&list);
+    ASSERT_EQ(model.data(model.index(0), 0).toString(), "1");
+    ASSERT_EQ(model.data(model.index(1), 0).toString(), "2");
+    ASSERT_EQ(model.data(model.index(2), 0).toString(), "3");
+}
+
+TEST(ListMonitorListModelTest, RoleNames)
+{
+    ListMonitorListModel model;
+    QHash<int, QByteArray> names({ { 0, "value" } });
+    ASSERT_EQ(model.roleNames(), names);
+}
diff --git a/test/monitor_models/listmonitormodel_test.cpp b/test/monitor_models/listmonitormodel_test.cpp
new file mode 100644
index 0000000..84bc740
--- /dev/null
+++ b/test/monitor_models/listmonitormodel_test.cpp
@@ -0,0 +1,130 @@
+#include <QtTest/QSignalSpy>
+#include <scratchcpp/virtualmachine.h>
+#include <scratchcpp/list.h>
+#include <listmonitormodel.h>
+#include <listmonitorlistmodel.h>
+#include <blocksectionmock.h>
+
+#include "../common.h"
+
+using namespace scratchcpprender;
+using namespace libscratchcpp;
+
+using ::testing::Return;
+
+TEST(ListMonitorModelTest, Constructors)
+{
+    {
+        ListMonitorModel model1;
+        ListMonitorModel model2(&model1);
+        ASSERT_EQ(model2.parent(), &model1);
+    }
+
+    {
+        ListMonitorModel model1;
+        ListMonitorModel model2(nullptr, &model1);
+        ASSERT_EQ(model2.parent(), &model1);
+    }
+}
+
+TEST(ListMonitorModelTest, OnValueChanged)
+{
+    ListMonitorModel model;
+    ListMonitorListModel *listModel = model.listModel();
+    VirtualMachine vm;
+
+    List list1("", "");
+    list1.push_back(1);
+    list1.push_back(2);
+
+    List list2("", "");
+    list2.push_back(1);
+    list2.push_back(2);
+    list2.push_back(3);
+    list2.push_back(4);
+
+    List list3("", "");
+    list3.push_back(1);
+    list3.push_back(2);
+    list3.push_back(3);
+
+    List *lists[] = { &list1, &list2, &list3 };
+    vm.setLists(lists);
+
+    vm.addReturnValue(1);
+    model.onValueChanged(&vm);
+    ASSERT_EQ(listModel->rowCount(QModelIndex()), 4);
+
+    vm.reset();
+    vm.addReturnValue(2);
+    model.onValueChanged(&vm);
+    ASSERT_EQ(listModel->rowCount(QModelIndex()), 3);
+
+    vm.reset();
+    vm.addReturnValue(0);
+    model.onValueChanged(&vm);
+    ASSERT_EQ(listModel->rowCount(QModelIndex()), 2);
+}
+
+TEST(ListMonitorModelTest, Type)
+{
+    ListMonitorModel model;
+    ASSERT_EQ(model.type(), MonitorModel::Type::List);
+}
+
+TEST(ListMonitorModelTest, Color)
+{
+    {
+        ListMonitorModel model;
+        ASSERT_EQ(model.color(), Qt::green);
+    }
+
+    {
+        ListMonitorModel model(nullptr, nullptr);
+        ASSERT_EQ(model.color(), Qt::green);
+    }
+
+    BlockSectionMock section;
+
+    {
+        // Invalid
+        EXPECT_CALL(section, name()).WillOnce(Return(""));
+        ListMonitorModel model(&section);
+        ASSERT_EQ(model.color(), Qt::green);
+    }
+
+    {
+        // Motion
+        EXPECT_CALL(section, name()).WillOnce(Return("Motion"));
+        ListMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#4C97FF"));
+    }
+
+    {
+        // Looks
+        EXPECT_CALL(section, name()).WillOnce(Return("Looks"));
+        ListMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#9966FF"));
+    }
+
+    {
+        // Sound
+        EXPECT_CALL(section, name()).WillOnce(Return("Sound"));
+        ListMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#CF63CF"));
+    }
+
+    {
+        // Variables
+        EXPECT_CALL(section, name()).WillOnce(Return("Variables"));
+        ListMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#FF8C1A"));
+    }
+
+    {
+        // Lists
+        EXPECT_CALL(section, name()).WillOnce(Return("Lists"));
+        ListMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#FF661A"));
+    }
+}
diff --git a/test/monitor_models/monitormodel_test.cpp b/test/monitor_models/monitormodel_test.cpp
new file mode 100644
index 0000000..f02373a
--- /dev/null
+++ b/test/monitor_models/monitormodel_test.cpp
@@ -0,0 +1,136 @@
+#include <QtTest/QSignalSpy>
+#include <scratchcpp/monitor.h>
+#include <scratchcpp/sprite.h>
+#include <monitormodel.h>
+
+#include "../common.h"
+
+using namespace scratchcpprender;
+using namespace libscratchcpp;
+
+TEST(MonitorModelTest, Constructors)
+{
+    MonitorModel model1;
+    MonitorModel model2(&model1);
+    ASSERT_EQ(model2.parent(), &model1);
+}
+
+TEST(MonitorModelTest, Init)
+{
+    MonitorModel model;
+    ASSERT_EQ(model.monitor(), nullptr);
+
+    Monitor monitor("", "");
+    model.init(&monitor);
+    ASSERT_EQ(model.monitor(), &monitor);
+}
+
+TEST(MonitorModelTest, OnVisibleChanged)
+{
+    MonitorModel model;
+    QSignalSpy spy(&model, &MonitorModel::visibleChanged);
+
+    model.onVisibleChanged(false);
+    ASSERT_EQ(spy.count(), 1);
+
+    model.onVisibleChanged(true);
+    ASSERT_EQ(spy.count(), 2);
+}
+
+TEST(MonitorModelTest, Name)
+{
+    MonitorModel model;
+    Monitor monitor("", "");
+    // TODO: Use monitor.setName()
+    const_cast<std::string *>(&monitor.name())->assign("days since 2000");
+    model.init(&monitor);
+    ASSERT_EQ(model.name().toStdString(), monitor.name());
+
+    Sprite sprite;
+    sprite.setName("Sprite2");
+    monitor.setSprite(&sprite);
+    // TODO: Use monitor.setName()
+    const_cast<std::string *>(&monitor.name())->assign("x position");
+    ASSERT_EQ(model.name().toStdString(), sprite.name() + ": " + monitor.name());
+}
+
+TEST(MonitorModelTest, Visible)
+{
+    MonitorModel model;
+    Monitor monitor("", "");
+    monitor.setVisible(true);
+    model.init(&monitor);
+    ASSERT_TRUE(model.visible());
+
+    monitor.setVisible(false);
+    ASSERT_FALSE(model.visible());
+
+    monitor.setVisible(false);
+    ASSERT_FALSE(model.visible());
+}
+
+TEST(MonitorModelTest, Type)
+{
+    MonitorModel model;
+    ASSERT_EQ(model.type(), MonitorModel::Type::Invalid);
+}
+
+TEST(MonitorModelTest, X)
+{
+    MonitorModel model;
+    Monitor monitor("", "");
+    monitor.setX(65);
+    model.init(&monitor);
+    ASSERT_EQ(model.x(), 65);
+
+    monitor.setX(-2);
+    ASSERT_EQ(model.x(), -2);
+}
+
+TEST(MonitorModelTest, Y)
+{
+    MonitorModel model;
+    Monitor monitor("", "");
+    monitor.setY(15);
+    model.init(&monitor);
+    ASSERT_EQ(model.y(), 15);
+
+    monitor.setY(-8);
+    ASSERT_EQ(model.y(), -8);
+}
+
+TEST(MonitorModelTest, Width)
+{
+    MonitorModel model;
+    Monitor monitor("", "");
+    QSignalSpy spy(&model, &MonitorModel::widthChanged);
+    monitor.setWidth(20);
+    model.init(&monitor);
+    ASSERT_EQ(model.width(), 20);
+
+    monitor.setWidth(150);
+    ASSERT_EQ(model.width(), 150);
+
+    model.setWidth(87);
+    ASSERT_EQ(model.width(), 87);
+    ASSERT_EQ(monitor.width(), 87);
+    ASSERT_EQ(spy.count(), 1);
+}
+
+TEST(MonitorModelTest, Height)
+{
+    MonitorModel model;
+    Monitor monitor("", "");
+    QSignalSpy spy(&model, &MonitorModel::heightChanged);
+    monitor.setHeight(20);
+    model.init(&monitor);
+    ASSERT_EQ(model.height(), 20);
+
+    monitor.setHeight(150);
+    ASSERT_EQ(model.height(), 150);
+
+    model.setHeight(87);
+    ASSERT_EQ(model.height(), 87);
+    ASSERT_EQ(monitor.height(), 87);
+    ASSERT_EQ(spy.count(), 1);
+}
diff --git a/test/monitor_models/valuemonitormodel_test.cpp b/test/monitor_models/valuemonitormodel_test.cpp
new file mode 100644
index 0000000..a1000e7
--- /dev/null
+++ b/test/monitor_models/valuemonitormodel_test.cpp
@@ -0,0 +1,193 @@
+#include <QtTest/QSignalSpy>
+#include <scratchcpp/monitor.h>
+#include <scratchcpp/virtualmachine.h>
+#include <scratchcpp/block.h>
+#include <scratchcpp/comment.h>
+#include <valuemonitormodel.h>
+#include <blocksectionmock.h>
+
+#include "../common.h"
+
+using namespace scratchcpprender;
+using namespace libscratchcpp;
+
+using ::testing::Return;
+
+TEST(ValueMonitorModelTest, Constructors)
+{
+    {
+        ValueMonitorModel model1;
+        ValueMonitorModel model2(&model1);
+        ASSERT_EQ(model2.parent(), &model1);
+    }
+
+    {
+        ValueMonitorModel model1;
+        ValueMonitorModel model2(nullptr, &model1);
+        ASSERT_EQ(model2.parent(), &model1);
+    }
+}
+
+TEST(ValueMonitorModelTest, OnValueChanged)
+{
+    ValueMonitorModel model;
+    QSignalSpy spy(&model, &ValueMonitorModel::valueChanged);
+    VirtualMachine vm;
+
+    vm.addReturnValue(5.4);
+    model.onValueChanged(&vm);
+    ASSERT_EQ(model.value(), "5.4");
+    ASSERT_EQ(spy.count(), 1);
+
+    vm.reset();
+    vm.addReturnValue("test");
+    model.onValueChanged(&vm);
+    ASSERT_EQ(model.value(), "test");
+    ASSERT_EQ(spy.count(), 2);
+}
+
+TEST(ValueMonitorModelTest, Value)
+{
+    ValueMonitorModel model;
+    QSignalSpy spy(&model, &ValueMonitorModel::valueChanged);
+    ASSERT_TRUE(model.value().isEmpty());
+
+    model.setValue("hello");
+    ASSERT_EQ(model.value(), "hello");
+    ASSERT_EQ(spy.count(), 1);
+
+    model.setValue("world");
+    ASSERT_EQ(model.value(), "world");
+    ASSERT_EQ(spy.count(), 2);
+
+    Monitor monitor("", "");
+    monitor.setValueChangeFunction([](Block *block, const Value &newValue) {
+        auto comment = std::make_shared<Comment>("");
+        comment->setText("it works");
+        block->setComment(comment);
+    });
+    model.init(&monitor);
+
+    model.setValue("test");
+    ASSERT_EQ(model.value(), "world");
+    ASSERT_TRUE(monitor.block()->comment());
+    ASSERT_EQ(monitor.block()->comment()->text(), "it works");
+    ASSERT_EQ(spy.count(), 2);
+}
+
+TEST(ValueMonitorModelTest, Type)
+{
+    ValueMonitorModel model;
+    ASSERT_EQ(model.type(), MonitorModel::Type::Value);
+}
+
+TEST(ValueMonitorModelTest, Color)
+{
+    {
+        ValueMonitorModel model;
+        ASSERT_EQ(model.color(), Qt::green);
+    }
+
+    {
+        ValueMonitorModel model(nullptr, nullptr);
+        ASSERT_EQ(model.color(), Qt::green);
+    }
+
+    BlockSectionMock section;
+
+    {
+        // Invalid
+        EXPECT_CALL(section, name()).WillOnce(Return(""));
+        ValueMonitorModel model(&section);
+        ASSERT_EQ(model.color(), Qt::green);
+    }
+
+    {
+        // Motion
+        EXPECT_CALL(section, name()).WillOnce(Return("Motion"));
+        ValueMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#4C97FF"));
+    }
+
+    {
+        // Looks
+        EXPECT_CALL(section, name()).WillOnce(Return("Looks"));
+        ValueMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#9966FF"));
+    }
+
+    {
+        // Sound
+        EXPECT_CALL(section, name()).WillOnce(Return("Sound"));
+        ValueMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#CF63CF"));
+    }
+
+    {
+        // Variables
+        EXPECT_CALL(section, name()).WillOnce(Return("Variables"));
+        ValueMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#FF8C1A"));
+    }
+
+    {
+        // Lists
+        EXPECT_CALL(section, name()).WillOnce(Return("Lists"));
+        ValueMonitorModel model(&section);
+        ASSERT_EQ(model.color(), QColor::fromString("#FF661A"));
+    }
+}
+
+TEST(ValueMonitorModelTest, Mode)
+{
+    ValueMonitorModel model;
+    Monitor monitor("", "");
+    monitor.setMode(Monitor::Mode::Default);
+    model.init(&monitor);
+    ASSERT_EQ(model.mode(), ValueMonitorModel::Mode::Default);
+
+    monitor.setMode(Monitor::Mode::Large);
+    ASSERT_EQ(model.mode(), ValueMonitorModel::Mode::Large);
+
+    monitor.setMode(Monitor::Mode::Slider);
+    ASSERT_EQ(model.mode(), ValueMonitorModel::Mode::Slider);
+}
+
+TEST(ValueMonitorModelTest, SliderMin)
+{
+    ValueMonitorModel model;
+    Monitor monitor("", "");
+    monitor.setSliderMin(-0.5);
+    model.init(&monitor);
+    ASSERT_EQ(model.sliderMin(), -0.5);
+
+    monitor.setSliderMin(2.65);
+    ASSERT_EQ(model.sliderMin(), 2.65);
+}
+
+TEST(ValueMonitorModelTest, SliderMax)
+{
+    ValueMonitorModel model;
+    Monitor monitor("", "");
+    monitor.setSliderMax(-0.5);
+    model.init(&monitor);
+    ASSERT_EQ(model.sliderMax(), -0.5);
+
+    monitor.setSliderMax(2.65);
+    ASSERT_EQ(model.sliderMax(), 2.65);
+}
+
+TEST(ValueMonitorModelTest, Discrete)
+{
+    ValueMonitorModel model;
+    Monitor monitor("", "");
+    monitor.setDiscrete(true);
+    model.init(&monitor);
+    ASSERT_TRUE(model.discrete());
+
+    monitor.setDiscrete(false);
+    ASSERT_FALSE(model.discrete());
+
+    monitor.setDiscrete(false);
+    ASSERT_FALSE(model.discrete());
+}
diff --git a/test/mouseeventhandler/CMakeLists.txt b/test/mouseeventhandler/CMakeLists.txt
new file mode 100644
index 0000000..0c160ab
--- /dev/null
+++ b/test/mouseeventhandler/CMakeLists.txt
@@ -0,0 +1,17 @@
+add_executable(
+  mouseeventhandler_test
+  mouseeventhandler_test.cpp
+)
+
+target_link_libraries(
+  mouseeventhandler_test
+  GTest::gtest_main
+  GTest::gmock_main
+  scratchcpp-render
+  scratchcpprender_mocks
+  ${QT_LIBS}
+  Qt6::Test
+)
+
+add_test(mouseeventhandler_test)
+gtest_discover_tests(mouseeventhandler_test)
diff --git a/test/mouseeventhandler/mouseeventhandler_test.cpp b/test/mouseeventhandler/mouseeventhandler_test.cpp
new file mode 100644
index 0000000..ab0134a
--- /dev/null
+++ b/test/mouseeventhandler/mouseeventhandler_test.cpp
@@ -0,0 +1,407 @@
+#include <QtTest/QSignalSpy>
+#include <scratchcpp/sprite.h>
+#include <mouseeventhandler.h>
+#include <projectloader.h>
+#include <spritemodel.h>
+#include <renderedtargetmock.h>
+
+#include "../common.h"
+
+using namespace scratchcpprender;
+using namespace libscratchcpp;
+
+using ::testing::Return;
+using ::testing::WithArgs;
+using ::testing::Invoke;
+using ::testing::_;
+
+TEST(SceneMouseAreaTest, Constructors)
+{
+    MouseEventHandler handler1;
+    MouseEventHandler handler2(&handler1);
+    ASSERT_EQ(handler2.parent(), &handler1);
+}
+
+TEST(MouseEventHandlerTest, Stage)
+{
+    MouseEventHandler handler;
+    ASSERT_EQ(handler.stage(), nullptr);
+
+    RenderedTargetMock stage;
+    handler.setStage(&stage);
+    ASSERT_EQ(handler.stage(), &stage);
+}
+
+TEST(MouseEventHandlerTest, ProjectLoader)
+{
+    MouseEventHandler handler;
+    ASSERT_EQ(handler.projectLoader(), nullptr);
+
+    ProjectLoader loader;
+    handler.setProjectLoader(&loader);
+    ASSERT_EQ(handler.projectLoader(), &loader);
+}
+
+TEST(MouseEventHandlerTest, HoverEnterLeaveEvent)
+{
+    MouseEventHandler handler;
+    RenderedTargetMock stage, renderedTarget1, renderedTarget2, renderedTarget3, renderedTarget4;
+    SpriteModel model1, model2, model3, model4;
+    model1.setRenderedTarget(&renderedTarget1);
+    model2.setRenderedTarget(&renderedTarget2);
+    model3.setRenderedTarget(&renderedTarget3);
+    model4.setRenderedTarget(&renderedTarget4);
+    Sprite sprite1, sprite2, sprite3, sprite4; // sprite1, sprite3 and sprite4 are clones here
+    sprite1.setLayerOrder(2);
+    sprite2.setLayerOrder(1);
+    sprite3.setLayerOrder(3);
+    sprite4.setLayerOrder(4);
+    ProjectLoader loader;
+    auto sprites = loader.sprites();
+    sprites.append(&sprites, &model2);
+    handler.setStage(&stage);
+    handler.setProjectLoader(&loader);
+    QPointingDevice dev;
+
+    static const QPointF localPos(8.5, 2.9);
+    static const QPointF scenePos(10.5, 4.9);
+    static const QPointF globalPos(11.5, 5.9);
+    static const QPointF oldPos(9.5, 3.9);
+
+    EXPECT_CALL(renderedTarget2, scratchTarget()).WillOnce(Return(&sprite2));
+    emit loader.spritesChanged();
+
+    emit loader.cloneCreated(&model1);
+    emit loader.cloneCreated(&model3);
+    emit loader.cloneCreated(&model4);
+    emit loader.cloneDeleted(&model4); // sprite4 was deleted
+
+    EXPECT_CALL(renderedTarget1, scratchTarget()).WillRepeatedly(Return(&sprite1));
+    EXPECT_CALL(renderedTarget2, scratchTarget()).WillRepeatedly(Return(&sprite2));
+    EXPECT_CALL(renderedTarget3, scratchTarget()).WillRepeatedly(Return(&sprite3));
+
+    EXPECT_CALL(renderedTarget1, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget2, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget3, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+
+    {
+        // HoverEnter
+        QHoverEvent event(QEvent::HoverEnter, scenePos, globalPos, oldPos, Qt::NoModifier, &dev);
+
+        EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(true));
+        EXPECT_CALL(renderedTarget1, hoverEnterEvent(&event));
+        ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+
+        EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(stage, hoverEnterEvent(&event));
+        ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    }
+
+    {
+        // HoverLeave
+        QHoverEvent event(QEvent::HoverLeave, scenePos, globalPos, oldPos, Qt::NoModifier, &dev);
+
+        EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(true));
+        EXPECT_CALL(renderedTarget2, hoverLeaveEvent(&event));
+        ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+
+        EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(false));
+        EXPECT_CALL(stage, hoverLeaveEvent(&event));
+        ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    }
+}
+
+TEST(MouseEventHandlerTest, HoverMoveEvent)
+{
+    MouseEventHandler handler;
+    RenderedTargetMock stage, renderedTarget1, renderedTarget2, renderedTarget3;
+    SpriteModel model1, model2, model3;
+    model1.setRenderedTarget(&renderedTarget1);
+    model2.setRenderedTarget(&renderedTarget2);
+    model3.setRenderedTarget(&renderedTarget3);
+    Sprite sprite1, sprite2, sprite3;
+    sprite1.setLayerOrder(2);
+    sprite2.setLayerOrder(1);
+    sprite3.setLayerOrder(3);
+    ProjectLoader loader;
+    auto sprites = loader.sprites();
+    sprites.append(&sprites, &model1);
+    sprites.append(&sprites, &model2);
+    sprites.append(&sprites, &model3);
+    handler.setStage(&stage);
+    handler.setProjectLoader(&loader);
+    QPointingDevice dev;
+
+    static const QPointF localPos(8.5, 2.9);
+    static const QPointF scenePos(10.5, 4.9);
+    static const QPointF globalPos(11.5, 5.9);
+    static const QPointF oldPos(9.5, 3.9);
+
+    EXPECT_CALL(renderedTarget1, scratchTarget()).WillRepeatedly(Return(&sprite1));
+    EXPECT_CALL(renderedTarget2, scratchTarget()).WillRepeatedly(Return(&sprite2));
+    EXPECT_CALL(renderedTarget3, scratchTarget()).WillRepeatedly(Return(&sprite3));
+    emit loader.spritesChanged();
+
+    EXPECT_CALL(renderedTarget1, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget2, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget3, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+
+    QHoverEvent event(QEvent::HoverMove, scenePos, globalPos, oldPos, Qt::ShiftModifier, &dev);
+    QSignalSpy spy(&handler, &MouseEventHandler::mouseMoved);
+
+    auto checkEnterLeaveEvent = [&dev](QHoverEvent *event, bool enter) {
+        ASSERT_TRUE(event);
+        ASSERT_EQ(event->type(), enter ? QEvent::HoverEnter : QEvent::HoverLeave);
+        ASSERT_EQ(event->scenePosition(), scenePos);
+        ASSERT_EQ(event->globalPosition(), globalPos);
+        ASSERT_EQ(event->oldPosF(), oldPos);
+        ASSERT_EQ(event->modifiers(), Qt::ShiftModifier);
+        ASSERT_EQ(event->device(), &dev);
+    };
+
+    auto checkEnterEvent = [checkEnterLeaveEvent](QHoverEvent *event) { checkEnterLeaveEvent(event, true); };
+    auto checkLeaveEvent = [checkEnterLeaveEvent](QHoverEvent *event) { checkEnterLeaveEvent(event, false); };
+
+    // Send to sprite 1
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, hoverEnterEvent(_)).WillOnce(WithArgs<0>(Invoke(checkEnterEvent)));
+    EXPECT_CALL(renderedTarget1, hoverMoveEvent(&event));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    ASSERT_EQ(spy.count(), 1);
+    spy.clear();
+
+    // Send to sprite 2
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, hoverLeaveEvent(_)).WillOnce(WithArgs<0>(Invoke(checkLeaveEvent)));
+    EXPECT_CALL(renderedTarget2, hoverEnterEvent(_)).WillOnce(WithArgs<0>(Invoke(checkEnterEvent)));
+    EXPECT_CALL(renderedTarget2, hoverMoveEvent(&event));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    ASSERT_EQ(spy.count(), 1);
+    spy.clear();
+
+    // Send to stage
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget2, hoverLeaveEvent(_)).WillOnce(WithArgs<0>(Invoke(checkLeaveEvent)));
+    EXPECT_CALL(stage, hoverEnterEvent(_)).WillOnce(WithArgs<0>(Invoke(checkEnterEvent)));
+    EXPECT_CALL(stage, hoverMoveEvent(&event));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    ASSERT_EQ(spy.count(), 1);
+    spy.clear();
+
+    // Send to sprite 3
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(stage, hoverLeaveEvent(_)).WillOnce(WithArgs<0>(Invoke(checkLeaveEvent)));
+    EXPECT_CALL(renderedTarget3, hoverEnterEvent(_)).WillOnce(WithArgs<0>(Invoke(checkEnterEvent)));
+    EXPECT_CALL(renderedTarget3, hoverMoveEvent(&event));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    ASSERT_EQ(spy.count(), 1);
+    spy.clear();
+}
+
+TEST(MouseEventHandlerTest, MouseMoveEvent)
+{
+    MouseEventHandler handler;
+    RenderedTargetMock stage, renderedTarget1, renderedTarget2, renderedTarget3;
+    SpriteModel model1, model2, model3;
+    model1.setRenderedTarget(&renderedTarget1);
+    model2.setRenderedTarget(&renderedTarget2);
+    model3.setRenderedTarget(&renderedTarget3);
+    Sprite sprite1, sprite2, sprite3;
+    sprite1.setLayerOrder(2);
+    sprite2.setLayerOrder(1);
+    sprite3.setLayerOrder(3);
+    ProjectLoader loader;
+    auto sprites = loader.sprites();
+    sprites.append(&sprites, &model1);
+    sprites.append(&sprites, &model2);
+    sprites.append(&sprites, &model3);
+    handler.setStage(&stage);
+    handler.setProjectLoader(&loader);
+    QPointingDevice dev;
+
+    static const QPointF localPos(8.5, 2.9);
+    static const QPointF scenePos(10.5, 4.9);
+    static const QPointF globalPos(11.5, 5.9);
+    static const QPointF oldPos(9.5, 3.9);
+
+    EXPECT_CALL(renderedTarget1, scratchTarget()).WillRepeatedly(Return(&sprite1));
+    EXPECT_CALL(renderedTarget2, scratchTarget()).WillRepeatedly(Return(&sprite2));
+    EXPECT_CALL(renderedTarget3, scratchTarget()).WillRepeatedly(Return(&sprite3));
+    emit loader.spritesChanged();
+
+    EXPECT_CALL(renderedTarget1, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget2, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget3, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+
+    QMouseEvent event(QEvent::MouseMove, scenePos, scenePos, globalPos, Qt::LeftButton, Qt::LeftButton | Qt::RightButton, Qt::ShiftModifier, &dev);
+    QSignalSpy spy(&handler, &MouseEventHandler::mouseMoved);
+
+    auto checkMoveEvent = [&dev](QMouseEvent *event) {
+        ASSERT_TRUE(event);
+        ASSERT_EQ(event->type(), QEvent::MouseMove);
+        ASSERT_EQ(event->position(), scenePos);
+        ASSERT_EQ(event->scenePosition(), scenePos);
+        ASSERT_EQ(event->globalPosition(), globalPos);
+        ASSERT_EQ(event->button(), Qt::LeftButton);
+        ASSERT_EQ(event->buttons(), Qt::LeftButton | Qt::RightButton);
+        ASSERT_EQ(event->modifiers(), Qt::ShiftModifier);
+        ASSERT_EQ(event->device(), &dev);
+    };
+
+    // Send to sprite 1
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, mouseMoveEvent(_)).WillOnce(WithArgs<0>(Invoke(checkMoveEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    ASSERT_EQ(spy.count(), 1);
+    spy.clear();
+
+    // Send to stage
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(stage, mouseMoveEvent(_)).WillOnce(WithArgs<0>(Invoke(checkMoveEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &event));
+    ASSERT_EQ(spy.count(), 1);
+    spy.clear();
+}
+
+TEST(MouseEventHandlerTest, MousePressReleaseEvent)
+{
+    MouseEventHandler handler;
+    RenderedTargetMock stage, renderedTarget1, renderedTarget2, renderedTarget3;
+    SpriteModel model1, model2, model3;
+    model1.setRenderedTarget(&renderedTarget1);
+    model2.setRenderedTarget(&renderedTarget2);
+    model3.setRenderedTarget(&renderedTarget3);
+    Sprite sprite1, sprite2, sprite3;
+    sprite1.setLayerOrder(2);
+    sprite2.setLayerOrder(1);
+    sprite3.setLayerOrder(3);
+    ProjectLoader loader;
+    auto sprites = loader.sprites();
+    sprites.append(&sprites, &model1);
+    sprites.append(&sprites, &model2);
+    sprites.append(&sprites, &model3);
+    handler.setStage(&stage);
+    handler.setProjectLoader(&loader);
+    QPointingDevice dev;
+
+    static const QPointF localPos(8.5, 2.9);
+    static const QPointF scenePos(10.5, 4.9);
+    static const QPointF globalPos(11.5, 5.9);
+    static const QPointF oldPos(9.5, 3.9);
+
+    EXPECT_CALL(renderedTarget1, scratchTarget()).WillRepeatedly(Return(&sprite1));
+    EXPECT_CALL(renderedTarget2, scratchTarget()).WillRepeatedly(Return(&sprite2));
+    EXPECT_CALL(renderedTarget3, scratchTarget()).WillRepeatedly(Return(&sprite3));
+    emit loader.spritesChanged();
+
+    EXPECT_CALL(renderedTarget1, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget2, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+    EXPECT_CALL(renderedTarget3, mapFromScene(scenePos)).WillRepeatedly(Return(localPos));
+
+    QMouseEvent pressEvent(QEvent::MouseButtonPress, scenePos, scenePos, globalPos, Qt::LeftButton, Qt::LeftButton | Qt::RightButton, Qt::ShiftModifier, &dev);
+    QMouseEvent releaseEvent(QEvent::MouseButtonRelease, scenePos, scenePos, globalPos, Qt::LeftButton, Qt::LeftButton | Qt::RightButton, Qt::ShiftModifier, &dev);
+    QSignalSpy pressedSpy(&handler, &MouseEventHandler::mousePressed);
+    QSignalSpy releasedSpy(&handler, &MouseEventHandler::mouseReleased);
+
+    auto checkMouseEvent = [&dev](QMouseEvent *event, QEvent::Type type) {
+        ASSERT_TRUE(event);
+        ASSERT_EQ(event->type(), type);
+        ASSERT_EQ(event->position(), scenePos);
+        ASSERT_EQ(event->scenePosition(), scenePos);
+        ASSERT_EQ(event->globalPosition(), globalPos);
+        ASSERT_EQ(event->button(), Qt::LeftButton);
+        ASSERT_EQ(event->buttons(), Qt::LeftButton | Qt::RightButton);
+        ASSERT_EQ(event->modifiers(), Qt::ShiftModifier);
+        ASSERT_EQ(event->device(), &dev);
+    };
+
+    auto checkPressEvent = [checkMouseEvent](QMouseEvent *event) { checkMouseEvent(event, QEvent::MouseButtonPress); };
+    auto checkReleaseEvent = [checkMouseEvent](QMouseEvent *event) { checkMouseEvent(event, QEvent::MouseButtonRelease); };
+
+    // Send to sprite 1 (press)
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, mousePressEvent(_)).WillOnce(WithArgs<0>(Invoke(checkPressEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &pressEvent));
+    ASSERT_EQ(pressedSpy.count(), 1);
+    ASSERT_EQ(releasedSpy.count(), 0);
+    pressedSpy.clear();
+    releasedSpy.clear();
+
+    // Send release (should be sent to sprite 1)
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, mouseReleaseEvent(_)).WillOnce(WithArgs<0>(Invoke(checkReleaseEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &releaseEvent));
+    ASSERT_EQ(pressedSpy.count(), 0);
+    ASSERT_EQ(releasedSpy.count(), 1);
+    pressedSpy.clear();
+    releasedSpy.clear();
+
+    // Send to sprite 1 (press)
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, mousePressEvent(_)).WillOnce(WithArgs<0>(Invoke(checkPressEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &pressEvent));
+    ASSERT_EQ(pressedSpy.count(), 1);
+    ASSERT_EQ(releasedSpy.count(), 0);
+    pressedSpy.clear();
+    releasedSpy.clear();
+
+    // Send release while sprite 3 is hovered (should be sent to both sprites)
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, mouseReleaseEvent(_)).WillOnce(WithArgs<0>(Invoke(checkReleaseEvent)));
+    EXPECT_CALL(renderedTarget3, mouseReleaseEvent(_)).WillOnce(WithArgs<0>(Invoke(checkReleaseEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &releaseEvent));
+    ASSERT_EQ(pressedSpy.count(), 0);
+    ASSERT_EQ(releasedSpy.count(), 1);
+    pressedSpy.clear();
+    releasedSpy.clear();
+
+    // Send to sprite 1 (release)
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(true));
+    EXPECT_CALL(renderedTarget1, mouseReleaseEvent(_)).WillOnce(WithArgs<0>(Invoke(checkReleaseEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &releaseEvent));
+    ASSERT_EQ(pressedSpy.count(), 0);
+    ASSERT_EQ(releasedSpy.count(), 1);
+    pressedSpy.clear();
+    releasedSpy.clear();
+
+    // Send to stage (press)
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(stage, mousePressEvent(_)).WillOnce(WithArgs<0>(Invoke(checkPressEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &pressEvent));
+    ASSERT_EQ(pressedSpy.count(), 1);
+    ASSERT_EQ(releasedSpy.count(), 0);
+    pressedSpy.clear();
+    releasedSpy.clear();
+
+    // Send release (should be sent to stage)
+    EXPECT_CALL(renderedTarget3, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget1, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(renderedTarget2, contains(localPos)).WillOnce(Return(false));
+    EXPECT_CALL(stage, mouseReleaseEvent(_)).WillOnce(WithArgs<0>(Invoke(checkReleaseEvent)));
+    ASSERT_TRUE(handler.eventFilter(nullptr, &releaseEvent));
+    ASSERT_EQ(pressedSpy.count(), 0);
+    ASSERT_EQ(releasedSpy.count(), 1);
+    pressedSpy.clear();
+    releasedSpy.clear();
+}
diff --git a/test/projectloader/CMakeLists.txt b/test/projectloader/CMakeLists.txt
index 9295d2f..c97209c 100644
--- a/test/projectloader/CMakeLists.txt
+++ b/test/projectloader/CMakeLists.txt
@@ -7,8 +7,8 @@ target_link_libraries(
   projectloader_test
   GTest::gtest_main
   GTest::gmock_main
-  libscratchcpp-gui
-  scratchcppgui_mocks
+  scratchcpp-render
+  scratchcpprender_mocks
   ${QT_LIBS}
   Qt6::Test
 )
diff --git a/test/projectloader/projectloader_test.cpp b/test/projectloader/projectloader_test.cpp
index 18033cc..84adb5c 100644
--- a/test/projectloader/projectloader_test.cpp
+++ b/test/projectloader/projectloader_test.cpp
@@ -1,11 +1,14 @@
 #include <QtTest/QSignalSpy>
 #include <projectloader.h>
 #include <spritemodel.h>
+#include <valuemonitormodel.h>
+#include <listmonitormodel.h>
 #include <enginemock.h>
+#include <renderedtargetmock.h>
 
 #include "../common.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 using namespace libscratchcpp;
 
 using ::testing::Return;
@@ -17,13 +20,14 @@ class ProjectLoaderTest : public testing::Test
         {
             static const std::chrono::milliseconds timeout(5000);
             auto startTime = std::chrono::steady_clock::now();
-            loader->setEventLoopEnabled(false);
             QSignalSpy fileNameSpy(loader, &ProjectLoader::fileNameChanged);
             QSignalSpy loadStatusSpy(loader, &ProjectLoader::loadStatusChanged);
             QSignalSpy loadingFinishedSpy(loader, &ProjectLoader::loadingFinished);
             QSignalSpy engineSpy(loader, &ProjectLoader::engineChanged);
             QSignalSpy stageSpy(loader, &ProjectLoader::stageChanged);
             QSignalSpy spritesSpy(loader, &ProjectLoader::spritesChanged);
+            QSignalSpy monitorsSpy(loader, &ProjectLoader::monitorsChanged);
+            QSignalSpy monitorAddedSpy(loader, &ProjectLoader::monitorAdded);
 
             loader->setFileName(fileName);
 
@@ -33,6 +37,8 @@ class ProjectLoaderTest : public testing::Test
             ASSERT_TRUE(engineSpy.empty());
             ASSERT_TRUE(stageSpy.empty());
             ASSERT_TRUE(spritesSpy.empty());
+            ASSERT_TRUE(monitorsSpy.empty());
+            ASSERT_TRUE(monitorAddedSpy.empty());
             ASSERT_EQ(loader->fileName(), fileName);
             ASSERT_FALSE(loader->loadStatus());
 
@@ -46,6 +52,8 @@ class ProjectLoaderTest : public testing::Test
             ASSERT_EQ(engineSpy.count(), 1);
             ASSERT_EQ(stageSpy.count(), 1);
             ASSERT_EQ(spritesSpy.count(), 1);
+            ASSERT_EQ(monitorsSpy.count(), loader->monitorList().size());
+            ASSERT_EQ(monitorAddedSpy.count(), loader->monitorList().size());
         }
 };
 
@@ -72,6 +80,55 @@ TEST_F(ProjectLoaderTest, Load)
     ASSERT_EQ(sprites.size(), 2);
     ASSERT_EQ(sprites[0]->sprite(), engine->targetAt(1));
     ASSERT_EQ(sprites[1]->sprite(), engine->targetAt(2));
+
+    const auto &monitors = loader.monitorList();
+    ASSERT_EQ(monitors.size(), 10);
+
+    ListMonitorModel *listMonitorModel = dynamic_cast<ListMonitorModel *>(monitors[0]);
+    ASSERT_EQ(listMonitorModel->monitor(), engine->monitors().at(0).get());
+    ASSERT_EQ(listMonitorModel->color(), QColor::fromString("#FF661A"));
+
+    ValueMonitorModel *valueMonitorModel = dynamic_cast<ValueMonitorModel *>(monitors[3]);
+    ASSERT_EQ(valueMonitorModel->monitor(), engine->monitors().at(3).get());
+    ASSERT_EQ(valueMonitorModel->color(), QColor::fromString("#FF8C1A"));
+}
+
+TEST_F(ProjectLoaderTest, Clones)
+{
+    ProjectLoader loader;
+    QSignalSpy cloneCreatedSpy(&loader, &ProjectLoader::cloneCreated);
+    QSignalSpy cloneDeletedSpy(&loader, &ProjectLoader::cloneDeleted);
+    QSignalSpy clonesChangedSpy(&loader, &ProjectLoader::clonesChanged);
+    load(&loader, "clones.sb3");
+    ASSERT_TRUE(cloneCreatedSpy.empty());
+    ASSERT_TRUE(cloneDeletedSpy.empty());
+    ASSERT_TRUE(clonesChangedSpy.empty());
+
+    auto engine = loader.engine();
+    engine->run();
+    ASSERT_EQ(cloneCreatedSpy.count(), 3);
+    ASSERT_EQ(cloneDeletedSpy.count(), 0);
+    ASSERT_EQ(clonesChangedSpy.count(), 3);
+
+    const auto &sprites = loader.spriteList();
+    const auto &clones = loader.cloneList();
+    ASSERT_EQ(sprites.size(), 1);
+    ASSERT_EQ(clones.size(), 3);
+    ASSERT_EQ(clones[0]->sprite()->cloneSprite(), sprites[0]->sprite());
+    ASSERT_EQ(clones[1]->sprite()->cloneSprite(), sprites[0]->sprite());
+    ASSERT_EQ(clones[2]->sprite()->cloneSprite(), sprites[0]->sprite());
+
+    RenderedTargetMock target1, target2, target3;
+    clones[0]->setRenderedTarget(&target1);
+    clones[1]->setRenderedTarget(&target2);
+    clones[2]->setRenderedTarget(&target3);
+
+    EXPECT_CALL(target2, deinitClone());
+    clones[1]->sprite()->deleteClone();
+    ASSERT_EQ(cloneCreatedSpy.count(), 3);
+    ASSERT_EQ(cloneDeletedSpy.count(), 1);
+    ASSERT_EQ(clonesChangedSpy.count(), 4);
+    ASSERT_EQ(clones.size(), 2);
 }
 
 TEST_F(ProjectLoaderTest, StartStop)
@@ -89,10 +146,20 @@ TEST_F(ProjectLoaderTest, StartStop)
     loader.stop();
 }
 
+TEST_F(ProjectLoaderTest, TimerEvent)
+{
+    ProjectLoader loader;
+    EngineMock engine;
+    loader.setEngine(&engine);
+    QTimerEvent event(0);
+
+    EXPECT_CALL(engine, step());
+    QCoreApplication::sendEvent(&loader, &event);
+}
+
 TEST_F(ProjectLoaderTest, Fps)
 {
     ProjectLoader loader;
-    loader.setEventLoopEnabled(false);
     EngineMock engine;
     loader.setEngine(&engine);
     ASSERT_EQ(loader.fps(), 30);
@@ -114,7 +181,6 @@ TEST_F(ProjectLoaderTest, Fps)
 TEST_F(ProjectLoaderTest, TurboMode)
 {
     ProjectLoader loader;
-    loader.setEventLoopEnabled(false);
     EngineMock engine;
     loader.setEngine(&engine);
     ASSERT_FALSE(loader.turboMode());
@@ -135,7 +201,6 @@ TEST_F(ProjectLoaderTest, TurboMode)
 TEST_F(ProjectLoaderTest, StageWidth)
 {
     ProjectLoader loader;
-    loader.setEventLoopEnabled(false);
     EngineMock engine;
     loader.setEngine(&engine);
     ASSERT_EQ(loader.stageWidth(), 480);
@@ -150,7 +215,6 @@ TEST_F(ProjectLoaderTest, StageWidth)
 TEST_F(ProjectLoaderTest, StageHeight)
 {
     ProjectLoader loader;
-    loader.setEventLoopEnabled(false);
     EngineMock engine;
     loader.setEngine(&engine);
     ASSERT_EQ(loader.stageHeight(), 360);
@@ -165,7 +229,6 @@ TEST_F(ProjectLoaderTest, StageHeight)
 TEST_F(ProjectLoaderTest, CloneLimit)
 {
     ProjectLoader loader;
-    loader.setEventLoopEnabled(false);
     EngineMock engine;
     loader.setEngine(&engine);
     ASSERT_EQ(loader.cloneLimit(), 300);
@@ -187,7 +250,6 @@ TEST_F(ProjectLoaderTest, CloneLimit)
 TEST_F(ProjectLoaderTest, SpriteFencing)
 {
     ProjectLoader loader;
-    loader.setEventLoopEnabled(false);
     EngineMock engine;
     loader.setEngine(&engine);
     ASSERT_TRUE(loader.spriteFencing());
@@ -204,29 +266,3 @@ TEST_F(ProjectLoaderTest, SpriteFencing)
     ASSERT_EQ(spy.count(), 1);
     ASSERT_TRUE(loader.spriteFencing());
 }
-
-TEST_F(ProjectLoaderTest, EventLoopEnabled)
-{
-    ProjectLoader loader;
-    EngineMock engine;
-    loader.setEngine(&engine);
-    ASSERT_TRUE(loader.eventLoopEnabled());
-
-    EXPECT_CALL(engine, stopEventLoop());
-    QSignalSpy spy(&loader, SIGNAL(eventLoopEnabledChanged()));
-    loader.setEventLoopEnabled(false);
-    ASSERT_EQ(spy.count(), 1);
-    ASSERT_FALSE(loader.eventLoopEnabled());
-
-    EXPECT_CALL(engine, runEventLoop());
-    spy.clear();
-    loader.setEventLoopEnabled(true);
-    ASSERT_EQ(spy.count(), 1);
-    ASSERT_TRUE(loader.eventLoopEnabled());
-
-    EXPECT_CALL(engine, stopEventLoop());
-    spy.clear();
-    loader.setEventLoopEnabled(false);
-    ASSERT_EQ(spy.count(), 1);
-    ASSERT_FALSE(loader.eventLoopEnabled());
-}
diff --git a/test/projectscene/CMakeLists.txt b/test/projectscene/CMakeLists.txt
index 9574849..d9bdf67 100644
--- a/test/projectscene/CMakeLists.txt
+++ b/test/projectscene/CMakeLists.txt
@@ -7,8 +7,8 @@ target_link_libraries(
   projectscene_test
   GTest::gtest_main
   GTest::gmock_main
-  libscratchcpp-gui
-  scratchcppgui_mocks
+  scratchcpp-render
+  scratchcpprender_mocks
   ${QT_LIBS}
 )
 
diff --git a/test/projectscene/projectscene_test.cpp b/test/projectscene/projectscene_test.cpp
index 9fc12a5..a534ffa 100644
--- a/test/projectscene/projectscene_test.cpp
+++ b/test/projectscene/projectscene_test.cpp
@@ -4,11 +4,11 @@
 
 #include "../common.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 
 using ::testing::Return;
 
-TEST(ProjectScene, Engine)
+TEST(ProjectSceneTest, Engine)
 {
     ProjectScene scene;
     ASSERT_EQ(scene.engine(), nullptr);
@@ -18,20 +18,30 @@ TEST(ProjectScene, Engine)
     ASSERT_EQ(scene.engine(), &engine);
 }
 
-TEST(ProjectScene, HandleMouseMove)
+TEST(ProjectSceneTest, StageScale)
+{
+    ProjectScene scene;
+    ASSERT_EQ(scene.stageScale(), 1);
+
+    scene.setStageScale(5.79);
+    ASSERT_EQ(scene.stageScale(), 5.79);
+}
+
+TEST(ProjectSceneTest, HandleMouseMove)
 {
     ProjectScene scene;
     EngineMock engine;
     scene.setEngine(&engine);
+    scene.setStageScale(2.5);
 
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(600));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(400));
-    EXPECT_CALL(engine, setMouseX(-253.1));
-    EXPECT_CALL(engine, setMouseY(216.7));
+    EXPECT_CALL(engine, setMouseX(-281.24));
+    EXPECT_CALL(engine, setMouseY(206.68));
     scene.handleMouseMove(46.9, -16.7);
 }
 
-TEST(ProjectScene, HandleMousePress)
+TEST(ProjectSceneTest, HandleMousePress)
 {
     ProjectScene scene;
     EngineMock engine;
@@ -44,7 +54,7 @@ TEST(ProjectScene, HandleMousePress)
     scene.handleMousePress();
 }
 
-TEST(ProjectScene, HandleMouseRelease)
+TEST(ProjectSceneTest, HandleMouseRelease)
 {
     ProjectScene scene;
     EngineMock engine;
@@ -57,7 +67,7 @@ TEST(ProjectScene, HandleMouseRelease)
     scene.handleMouseRelease();
 }
 
-TEST(ProjectScene, HandleKeyPressAndRelease)
+TEST(ProjectSceneTest, HandleKeyPressAndRelease)
 {
     static const std::unordered_map<Qt::Key, KeyEvent::Type> SPECIAL_KEY_MAP = {
         { Qt::Key_Space, KeyEvent::Type::Space }, { Qt::Key_Left, KeyEvent::Type::Left },    { Qt::Key_Up, KeyEvent::Type::Up },      { Qt::Key_Right, KeyEvent::Type::Right },
@@ -71,21 +81,41 @@ TEST(ProjectScene, HandleKeyPressAndRelease)
     for (const auto &[qtKey, scratchKey] : SPECIAL_KEY_MAP) {
         KeyEvent event(scratchKey);
         EXPECT_CALL(engine, setKeyState(event.name(), true));
+        EXPECT_CALL(engine, setAnyKeyPressed(true));
         scene.handleKeyPress(qtKey, "test");
 
         EXPECT_CALL(engine, setKeyState(event.name(), false));
+        EXPECT_CALL(engine, setAnyKeyPressed(false));
         scene.handleKeyRelease(qtKey, "test");
     }
 
     EXPECT_CALL(engine, setKeyState("a", true));
+    EXPECT_CALL(engine, setAnyKeyPressed(true));
     scene.handleKeyPress(Qt::Key_A, "a");
 
     EXPECT_CALL(engine, setKeyState("a", false));
+    EXPECT_CALL(engine, setAnyKeyPressed(false));
     scene.handleKeyRelease(Qt::Key_A, "a");
 
     EXPECT_CALL(engine, setKeyState("0", true));
+    EXPECT_CALL(engine, setAnyKeyPressed(true));
     scene.handleKeyPress(Qt::Key_0, "0");
 
     EXPECT_CALL(engine, setKeyState("0", false));
+    EXPECT_CALL(engine, setAnyKeyPressed(false));
     scene.handleKeyRelease(Qt::Key_0, "0");
+
+    EXPECT_CALL(engine, setAnyKeyPressed(true));
+    scene.handleKeyPress(Qt::Key_Control, "");
+
+    EXPECT_CALL(engine, setKeyState("a", true));
+    EXPECT_CALL(engine, setAnyKeyPressed(true));
+    scene.handleKeyPress(Qt::Key_A, "a");
+
+    EXPECT_CALL(engine, setKeyState("a", false));
+    EXPECT_CALL(engine, setAnyKeyPressed).Times(0);
+    scene.handleKeyRelease(Qt::Key_A, "a");
+
+    EXPECT_CALL(engine, setAnyKeyPressed(false));
+    scene.handleKeyRelease(Qt::Key_Control, "");
 }
diff --git a/test/renderedtarget/CMakeLists.txt b/test/renderedtarget/CMakeLists.txt
index 1b6b5b0..2c81f7f 100644
--- a/test/renderedtarget/CMakeLists.txt
+++ b/test/renderedtarget/CMakeLists.txt
@@ -7,8 +7,8 @@ target_link_libraries(
   renderedtarget_test
   GTest::gtest_main
   GTest::gmock_main
-  libscratchcpp-gui
-  scratchcppgui_mocks
+  scratchcpp-render
+  scratchcpprender_mocks
   ${QT_LIBS}
   Qt6::Test
   qnanopainter
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index 55dc454..46ea4a2 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -5,6 +5,7 @@
 #include <renderedtarget.h>
 #include <stagemodel.h>
 #include <spritemodel.h>
+#include <scenemousearea.h>
 #include <scratchcpp/stage.h>
 #include <scratchcpp/sprite.h>
 #include <scratchcpp/costume.h>
@@ -12,7 +13,7 @@
 
 #include "../common.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 using namespace libscratchcpp;
 
 using ::testing::Return;
@@ -47,9 +48,10 @@ TEST_F(RenderedTargetTest, Constructors)
     ASSERT_EQ(target2.parentItem(), &target1);
 }
 
-TEST_F(RenderedTargetTest, LoadAndUpdateProperties)
+TEST_F(RenderedTargetTest, UpdateMethods)
 {
-    RenderedTarget target;
+    RenderedTarget parent; // a parent item is needed for setVisible() to work
+    RenderedTarget target(&parent);
     QSignalSpy mirrorHorizontallySpy(&target, &RenderedTarget::mirrorHorizontallyChanged);
 
     // Stage
@@ -62,37 +64,21 @@ TEST_F(RenderedTargetTest, LoadAndUpdateProperties)
     costume.setData(costumeData.size(), static_cast<void *>(costumeData.data()));
     costume.setRotationCenterX(-23);
     costume.setRotationCenterY(72);
+    costume.setBitmapResolution(2.5);
     EngineMock engine;
-    target.loadCostume(&costume);
     target.setEngine(&engine);
 
-    target.setWidth(14.3);
-    target.setHeight(5.8);
-    target.setX(64.5);
-    target.setY(-43.7);
-    target.setZ(2.5);
-    target.setRotation(-78.05);
-    target.setTransformOriginPoint(QPointF(3.4, 9.7));
-
-    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(544));
-    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(249));
-    target.loadProperties();
-    ASSERT_EQ(target.width(), 14.3);
-    ASSERT_EQ(target.height(), 5.8);
-    ASSERT_EQ(target.x(), 64.5);
-    ASSERT_EQ(target.y(), -43.7);
-    ASSERT_EQ(target.z(), 2.5);
-    ASSERT_EQ(target.rotation(), -78.05);
-    ASSERT_EQ(target.transformOriginPoint(), QPointF(3.4, 9.7));
-
-    target.updateProperties();
-    ASSERT_EQ(target.width(), 4);
-    ASSERT_EQ(target.height(), 6);
-    ASSERT_EQ(target.x(), 295);
-    ASSERT_EQ(target.y(), 52.5);
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.loadCostume(&costume);
+    target.beforeRedraw();
+    ASSERT_EQ(target.width(), 1.6);
+    ASSERT_EQ(target.height(), 2.4);
+    ASSERT_EQ(target.x(), 249.2);
+    ASSERT_EQ(target.y(), 151.2);
     ASSERT_EQ(target.z(), 0);
     ASSERT_EQ(target.rotation(), 0);
-    ASSERT_EQ(target.transformOriginPoint(), QPointF(-23, 72));
+    ASSERT_EQ(target.transformOriginPoint(), QPointF(-9.2, 28.8));
 
     target.setStageModel(nullptr);
     ASSERT_TRUE(mirrorHorizontallySpy.empty());
@@ -103,78 +89,98 @@ TEST_F(RenderedTargetTest, LoadAndUpdateProperties)
     sprite.setRotationStyle(Sprite::RotationStyle::AllAround);
     sprite.setDirection(-67.16);
     sprite.setSize(143.98);
-    sprite.setX(-67.94);
-    sprite.setY(121.76);
+    sprite.setX(0);
+    sprite.setY(0);
     sprite.setLayerOrder(3);
     SpriteModel spriteModel;
     sprite.setInterface(&spriteModel);
+
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     target.setSpriteModel(&spriteModel);
+    target.beforeRedraw();
 
-    target.setWidth(14.3);
-    target.setHeight(5.8);
-    target.setX(64.5);
-    target.setY(-43.7);
-    target.setZ(2.5);
-    target.setRotation(-78.05);
-    target.setTransformOriginPoint(QPointF(3.4, 9.7));
-
-    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(544));
-    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(249));
-    target.loadProperties();
-    ASSERT_EQ(target.width(), 14.3);
-    ASSERT_EQ(target.height(), 5.8);
-    ASSERT_EQ(target.x(), 64.5);
-    ASSERT_EQ(target.y(), -43.7);
-    ASSERT_EQ(target.z(), 2.5);
-    ASSERT_EQ(target.rotation(), -78.05);
-    ASSERT_EQ(target.transformOriginPoint(), QPointF(3.4, 9.7));
-
-    target.updateProperties();
-    ASSERT_EQ(target.width(), 14.3);
-    ASSERT_EQ(target.height(), 5.8);
-    ASSERT_EQ(std::round(target.x() * 100) / 100, 237.18);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, -100.93);
+    ASSERT_EQ(std::round(target.width() * 100) / 100, 2.3);
+    ASSERT_EQ(std::round(target.height() * 100) / 100, 3.46);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 253.25);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, 138.53);
     ASSERT_EQ(target.z(), 3);
     ASSERT_EQ(target.rotation(), -157.16);
-    ASSERT_EQ(target.transformOriginPoint().x(), -33.1154);
-    ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 103.67);
+    ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -13.25);
+    ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 41.47);
     ASSERT_TRUE(mirrorHorizontallySpy.empty());
 
-    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(544));
-    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(249));
-    sprite.setRotationStyle(Sprite::RotationStyle::LeftRight);
-    target.loadProperties();
-    ASSERT_EQ(target.mirrorHorizontally(), false);
-    ASSERT_EQ(target.rotation(), -157.16);
+    // Visibility
+    target.updateVisibility(false);
+    ASSERT_FALSE(target.isVisible());
+
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateVisibility(true);
+    ASSERT_TRUE(target.isVisible());
+
+    // X
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateX(12.5);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 265.75);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, 138.53);
+
+    // Y
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateY(-76.09);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 265.75);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, 214.62);
+
+    // Size
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateSize(56.2);
+    target.beforeRedraw();
+    ASSERT_EQ(std::round(target.width() * 100) / 100, 0.9);
+    ASSERT_EQ(std::round(target.height() * 100) / 100, 1.35);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9);
+    ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -5.17);
+    ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 16.19);
+
+    // Direction
+    target.updateDirection(123.8);
+    ASSERT_EQ(target.rotation(), 33.8);
     ASSERT_TRUE(mirrorHorizontallySpy.empty());
 
-    target.updateProperties();
-    ASSERT_EQ(target.mirrorHorizontally(), true);
+    // Rotation style
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateRotationStyle(Sprite::RotationStyle::LeftRight);
+    ASSERT_EQ(target.mirrorHorizontally(), false);
     ASSERT_EQ(target.rotation(), 0);
-    ASSERT_EQ(mirrorHorizontallySpy.count(), 1);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9);
+    ASSERT_TRUE(mirrorHorizontallySpy.empty());
 
-    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(544));
-    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(249));
-    sprite.setDirection(134.89);
-    target.loadProperties();
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateDirection(-15);
     ASSERT_EQ(target.mirrorHorizontally(), true);
     ASSERT_EQ(target.rotation(), 0);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 247.33);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9);
     ASSERT_EQ(mirrorHorizontallySpy.count(), 1);
 
-    target.updateProperties();
-    ASSERT_EQ(target.mirrorHorizontally(), false);
-    ASSERT_EQ(target.rotation(), 0);
-    ASSERT_EQ(mirrorHorizontallySpy.count(), 2);
-
-    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(544));
-    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(249));
-    sprite.setRotationStyle(Sprite::RotationStyle::DoNotRotate);
-    target.loadProperties();
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateDirection(134.89);
     ASSERT_EQ(target.mirrorHorizontally(), false);
     ASSERT_EQ(target.rotation(), 0);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9);
     ASSERT_EQ(mirrorHorizontallySpy.count(), 2);
 
-    target.updateProperties();
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateRotationStyle(Sprite::RotationStyle::DoNotRotate);
     ASSERT_EQ(target.mirrorHorizontally(), false);
     ASSERT_EQ(target.rotation(), 0);
     ASSERT_EQ(mirrorHorizontallySpy.count(), 2);
@@ -188,22 +194,12 @@ TEST_F(RenderedTargetTest, LoadJpegCostume)
     costume.setBitmapResolution(3);
 
     RenderedTarget target;
-
     target.loadCostume(&costume);
     ASSERT_FALSE(target.isSvg());
     ASSERT_FALSE(target.bitmapBuffer()->isOpen());
     target.bitmapBuffer()->open(QBuffer::ReadOnly);
-    ASSERT_TRUE(target.bitmapBuffer()->readAll().toStdString().empty());
-    ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty());
-    target.bitmapBuffer()->close();
-
-    target.updateProperties();
-    ASSERT_FALSE(target.isSvg());
-    ASSERT_FALSE(target.bitmapBuffer()->isOpen());
-    target.bitmapBuffer()->open(QBuffer::ReadOnly);
     ASSERT_EQ(target.bitmapBuffer()->readAll().toStdString(), str);
     ASSERT_EQ(target.bitmapUniqueKey().toStdString(), costume.id());
-    target.bitmapBuffer()->close();
 }
 
 TEST_F(RenderedTargetTest, LoadPngCostume)
@@ -214,34 +210,56 @@ TEST_F(RenderedTargetTest, LoadPngCostume)
     costume.setBitmapResolution(3);
 
     RenderedTarget target;
-
     target.loadCostume(&costume);
     ASSERT_FALSE(target.isSvg());
     ASSERT_FALSE(target.bitmapBuffer()->isOpen());
     target.bitmapBuffer()->open(QBuffer::ReadOnly);
-    ASSERT_TRUE(target.bitmapBuffer()->readAll().toStdString().empty());
-    ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty());
-    target.bitmapBuffer()->close();
-
-    target.updateProperties();
-    ASSERT_FALSE(target.isSvg());
-    ASSERT_FALSE(target.bitmapBuffer()->isOpen());
-    target.bitmapBuffer()->open(QBuffer::ReadOnly);
     ASSERT_EQ(target.bitmapBuffer()->readAll().toStdString(), str);
     ASSERT_EQ(target.bitmapUniqueKey().toStdString(), costume.id());
-    target.bitmapBuffer()->close();
 }
 
 TEST_F(RenderedTargetTest, LoadSvgCostume)
 {
+    // Get maximum viewport dimensions
+    QOpenGLContext context;
+    context.create();
+    Q_ASSERT(context.isValid());
+
+    QOffscreenSurface surface;
+    surface.create();
+    Q_ASSERT(surface.isValid());
+
+    context.makeCurrent(&surface);
+    GLint dims[2];
+    glGetIntegerv(GL_MAX_VIEWPORT_DIMS, dims);
+    double maxWidth = dims[0] * 0.1;
+    double maxHeight = dims[1] * 0.1;
+    double maxSize = std::min(maxWidth / (1143 / 90.0), maxHeight / (1143 / 90.0));
+    context.doneCurrent();
+
     std::string str = readFileStr("image.svg");
-    Costume costume("", "abc", "svg");
-    costume.setData(str.size(), static_cast<void *>(const_cast<char *>(str.c_str())));
-    costume.setBitmapResolution(3);
+    auto costume = std::make_shared<Costume>("", "abc", "svg");
+    costume->setData(str.size(), static_cast<void *>(const_cast<char *>(str.c_str())));
+    costume->setBitmapResolution(1);
+
+    EngineMock engine;
+    SpriteModel model;
+    Sprite sprite;
+    sprite.setSize(maxSize * 100);
+    sprite.setX(49.7);
+    sprite.setY(-64.15);
+    costume->setRotationCenterX(-84);
+    costume->setRotationCenterY(53);
+    model.init(&sprite);
 
     RenderedTarget target;
+    target.setEngine(&engine);
 
-    target.loadCostume(&costume);
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.setSpriteModel(&model);
+    target.loadCostume(costume.get());
+    target.beforeRedraw();
     ASSERT_TRUE(target.isSvg());
     ASSERT_FALSE(target.bitmapBuffer()->isOpen());
     target.bitmapBuffer()->open(QBuffer::ReadOnly);
@@ -249,13 +267,42 @@ TEST_F(RenderedTargetTest, LoadSvgCostume)
     ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty());
     target.bitmapBuffer()->close();
 
-    target.updateProperties();
-    ASSERT_TRUE(target.isSvg());
-    ASSERT_FALSE(target.bitmapBuffer()->isOpen());
-    target.bitmapBuffer()->open(QBuffer::ReadOnly);
-    ASSERT_TRUE(target.bitmapBuffer()->readAll().toStdString().empty());
-    ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty());
-    target.bitmapBuffer()->close();
+    ASSERT_EQ(std::round(target.width() * 100) / 100, 1548.09);
+    ASSERT_EQ(std::round(target.height() * 100) / 100, 1548.09);
+    ASSERT_EQ(target.scale(), 1);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 11126.36);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, -6593.27);
+    ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -10836.66);
+    ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 6837.42);
+
+    // Test scale limit
+    EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480));
+    EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360));
+    target.updateSize(maxSize * 250);
+    target.setStageScale(3.89);
+
+    ASSERT_EQ(std::round(target.width() * 100) / 100, 1548.09);
+    ASSERT_EQ(std::round(target.height() * 100) / 100, 1548.09);
+    ASSERT_EQ(std::round(target.scale() * 100) / 100, 9.19);
+    ASSERT_EQ(std::round(target.x() * 100) / 100, 12595.73);
+    ASSERT_EQ(std::round(target.y() * 100) / 100, -6286.52);
+    ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -11468.8);
+    ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 7236.27);
+}
+
+TEST_F(RenderedTargetTest, DeinitClone)
+{
+    RenderedTarget target1, target2;
+    SceneMouseArea mouseArea;
+    target1.setMouseArea(&mouseArea);
+
+    mouseArea.setDraggedSprite(&target2);
+    target1.deinitClone();
+    ASSERT_EQ(mouseArea.draggedSprite(), &target2);
+
+    mouseArea.setDraggedSprite(&target1);
+    target1.deinitClone();
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
 }
 
 TEST_F(RenderedTargetTest, PaintSvg)
@@ -265,16 +312,20 @@ TEST_F(RenderedTargetTest, PaintSvg)
     costume.setData(str.size(), static_cast<void *>(const_cast<char *>(str.c_str())));
     costume.setBitmapResolution(3);
 
+    EngineMock engine;
     Sprite sprite;
     sprite.setSize(2525.7);
 
     SpriteModel model;
     model.init(&sprite);
 
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     RenderedTarget target;
+    target.setEngine(&engine);
     target.setSpriteModel(&model);
     target.loadCostume(&costume);
-    target.updateProperties();
+    target.beforeRedraw();
 
     // Create OpenGL context
     QOpenGLContext context;
@@ -309,6 +360,219 @@ TEST_F(RenderedTargetTest, PaintSvg)
     context.doneCurrent();
 }
 
+TEST_F(RenderedTargetTest, HullPoints)
+{
+    EngineMock engine;
+    Sprite sprite;
+    SpriteModel model;
+    model.init(&sprite);
+
+    RenderedTarget target;
+    target.setEngine(&engine);
+    target.setSpriteModel(&model);
+
+    // Create OpenGL context
+    QOpenGLContext context;
+    QOffscreenSurface surface;
+    createContextAndSurface(&context, &surface);
+
+    // Create a painter
+    QNanoPainter painter;
+
+    QOpenGLFramebufferObjectFormat format;
+    format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
+
+    // Begin painting
+    QOpenGLFramebufferObject fbo(4, 6, format);
+    fbo.bind();
+    painter.beginFrame(fbo.width(), fbo.height());
+
+    // Paint
+    QNanoImage image = QNanoImage::fromCache(&painter, "image.png");
+    painter.drawImage(image, 0, 0);
+    painter.endFrame();
+
+    // Test hull points
+    target.setWidth(3);
+    target.setHeight(3);
+    target.updateHullPoints(&fbo);
+    ASSERT_EQ(target.hullPoints(), std::vector<QPointF>({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } }));
+
+    // Begin painting (multisampled)
+    format.setSamples(16);
+    QOpenGLFramebufferObject fboMultiSampled(4, 6, format);
+    fboMultiSampled.bind();
+    painter.beginFrame(fboMultiSampled.width(), fboMultiSampled.height());
+
+    // Paint (multisampled)
+    painter.drawImage(image, 0, 0);
+    painter.endFrame();
+
+    // Test hull points (this is undefined with multisampling, so we just check if there are any hull points)
+    ASSERT_FALSE(target.hullPoints().empty());
+
+    // Release
+    fbo.release();
+    context.doneCurrent();
+
+    // Test contains()
+    ASSERT_FALSE(target.contains({ 0, 0 }));
+    ASSERT_FALSE(target.contains({ 1, 0 }));
+    ASSERT_FALSE(target.contains({ 2, 0 }));
+    ASSERT_FALSE(target.contains({ 3, 0 }));
+
+    ASSERT_FALSE(target.contains({ 0, 1 }));
+    ASSERT_TRUE(target.contains({ 1, 1 }));
+    ASSERT_TRUE(target.contains({ 1.4, 1.25 }));
+    ASSERT_TRUE(target.contains({ 2, 1 }));
+    ASSERT_TRUE(target.contains({ 3, 1 }));
+
+    ASSERT_TRUE(target.contains({ 1, 2 }));
+    ASSERT_FALSE(target.contains({ 2, 2 }));
+    ASSERT_TRUE(target.contains({ 3, 2 }));
+    ASSERT_FALSE(target.contains({ 3.5, 2.1 }));
+
+    ASSERT_TRUE(target.contains({ 1, 3 }));
+    ASSERT_TRUE(target.contains({ 2, 3 }));
+    ASSERT_TRUE(target.contains({ 3, 3 }));
+    ASSERT_FALSE(target.contains({ 3.3, 3.5 }));
+}
+
+TEST_F(RenderedTargetTest, SpriteDragging)
+{
+    RenderedTarget target;
+    EngineMock engine;
+    target.setEngine(&engine);
+
+    SpriteModel model;
+    Sprite sprite;
+    sprite.setX(64.08);
+    sprite.setY(-6.86);
+    model.init(&sprite);
+    target.setSpriteModel(&model);
+    target.setStageScale(3.5);
+
+    SceneMouseArea mouseArea;
+    target.setMouseArea(&mouseArea);
+
+    emit mouseArea.mouseMoved(1064, 651);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+
+    // Try right mouse button (should not work)
+    QMouseEvent moveEventRightButton(QEvent::MouseMove, QPointF(), QPointF(), Qt::RightButton, Qt::RightButton, Qt::NoModifier);
+    QMouseEvent pressEventRightButton(QEvent::MouseButtonPress, QPointF(), QPointF(), Qt::RightButton, Qt::RightButton, Qt::NoModifier);
+    QMouseEvent releaseEventRightButton(QEvent::MouseButtonRelease, QPointF(), QPointF(), Qt::RightButton, Qt::RightButton, Qt::NoModifier);
+    EXPECT_CALL(engine, clickTarget(&sprite));
+    QCoreApplication::sendEvent(&target, &pressEventRightButton);
+    QCoreApplication::sendEvent(&target, &moveEventRightButton);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    EXPECT_CALL(engine, clickTarget).Times(0);
+    QCoreApplication::sendEvent(&target, &releaseEventRightButton);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+
+    emit mouseArea.mouseMoved(1064, 651);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+
+    // Try right mouse button with "draggable" set to true (should not work)
+    sprite.setDraggable(true);
+    EXPECT_CALL(engine, clickTarget).Times(0);
+    QCoreApplication::sendEvent(&target, &pressEventRightButton);
+    QCoreApplication::sendEvent(&target, &moveEventRightButton);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    EXPECT_CALL(engine, clickTarget(&sprite));
+    QCoreApplication::sendEvent(&target, &releaseEventRightButton);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+
+    emit mouseArea.mouseMoved(1064, 651);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+
+    // Try left mouse button (should not work with "draggable" set to false)
+    sprite.setDraggable(false);
+    QMouseEvent moveEvent(QEvent::MouseMove, QPointF(), QPointF(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+    QMouseEvent pressEvent(QEvent::MouseButtonPress, QPointF(), QPointF(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+    QMouseEvent releaseEvent(QEvent::MouseButtonRelease, QPointF(), QPointF(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+    EXPECT_CALL(engine, clickTarget(&sprite));
+    QCoreApplication::sendEvent(&target, &pressEvent);
+    QCoreApplication::sendEvent(&target, &moveEvent);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+
+    emit mouseArea.mouseMoved(1064, 651);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    EXPECT_CALL(engine, clickTarget).Times(0);
+    QCoreApplication::sendEvent(&target, &releaseEvent);
+
+    // Try left mouse button with "draggable" set to true
+    sprite.setDraggable(true);
+    EXPECT_CALL(engine, clickTarget).Times(0);
+    QCoreApplication::sendEvent(&target, &pressEvent);
+    EXPECT_CALL(engine, mouseX()).WillOnce(Return(67.95));
+    EXPECT_CALL(engine, mouseY()).WillOnce(Return(2.1));
+    EXPECT_CALL(engine, moveSpriteToFront(&sprite));
+    QCoreApplication::sendEvent(&target, &moveEvent);
+    ASSERT_EQ(sprite.x(), 64.08);
+    ASSERT_EQ(sprite.y(), -6.86);
+    ASSERT_EQ(mouseArea.draggedSprite(), &target);
+
+    // Drag
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    emit mouseArea.mouseMoved(1067.8, 649.06);
+    ASSERT_EQ(std::round(sprite.x() * 100) / 100, 61.22);
+    ASSERT_EQ(std::round(sprite.y() * 100) / 100, -14.41);
+    ASSERT_EQ(mouseArea.draggedSprite(), &target);
+
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    emit mouseArea.mouseMoved(1092.47, 605.46);
+    ASSERT_EQ(std::round(sprite.x() * 100) / 100, 68.26);
+    ASSERT_EQ(std::round(sprite.y() * 100) / 100, -1.95);
+    ASSERT_EQ(mouseArea.draggedSprite(), &target);
+
+    // Create another sprite
+    RenderedTarget anotherTarget;
+    anotherTarget.setEngine(&engine);
+
+    SpriteModel anotherModel;
+    Sprite anotherSprite;
+    anotherSprite.setX(64.08);
+    anotherSprite.setY(-6.86);
+    anotherSprite.setDraggable(true);
+    anotherModel.init(&anotherSprite);
+    anotherTarget.setSpriteModel(&model);
+    anotherTarget.setStageScale(3.5);
+    anotherTarget.setMouseArea(&mouseArea);
+
+    // Try to drag the second sprite while the first is being dragged
+    sprite.setDraggable(true);
+    EXPECT_CALL(engine, clickTarget).Times(0);
+    QCoreApplication::sendEvent(&anotherTarget, &pressEvent);
+    QCoreApplication::sendEvent(&anotherTarget, &moveEvent);
+    ASSERT_EQ(mouseArea.draggedSprite(), &target);
+    EXPECT_CALL(engine, clickTarget(&sprite));
+    QCoreApplication::sendEvent(&anotherTarget, &releaseEvent);
+
+    // Stop dragging
+    EXPECT_CALL(engine, clickTarget).Times(0);
+    QCoreApplication::sendEvent(&target, &releaseEvent);
+    ASSERT_EQ(std::round(sprite.x() * 100) / 100, 68.26);
+    ASSERT_EQ(std::round(sprite.y() * 100) / 100, -1.95);
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+}
+
 TEST_F(RenderedTargetTest, Engine)
 {
     RenderedTarget target;
@@ -357,3 +621,22 @@ TEST_F(RenderedTargetTest, ScratchTarget)
     target.setSpriteModel(&spriteModel);
     ASSERT_EQ(target.scratchTarget(), &sprite);
 }
+
+TEST_F(RenderedTargetTest, MouseArea)
+{
+    RenderedTarget target;
+    ASSERT_EQ(target.mouseArea(), nullptr);
+
+    SceneMouseArea mouseArea;
+    target.setMouseArea(&mouseArea);
+    ASSERT_EQ(target.mouseArea(), &mouseArea);
+}
+
+TEST_F(RenderedTargetTest, StageScale)
+{
+    RenderedTarget target;
+    ASSERT_EQ(target.stageScale(), 1);
+
+    target.setStageScale(6.4);
+    ASSERT_EQ(target.stageScale(), 6.4);
+}
diff --git a/test/scenemousearea/CMakeLists.txt b/test/scenemousearea/CMakeLists.txt
new file mode 100644
index 0000000..4672c92
--- /dev/null
+++ b/test/scenemousearea/CMakeLists.txt
@@ -0,0 +1,17 @@
+add_executable(
+  scenemousearea_test
+  scenemousearea_test.cpp
+)
+
+target_link_libraries(
+  scenemousearea_test
+  GTest::gtest_main
+  GTest::gmock_main
+  scratchcpp-render
+  scratchcpprender_mocks
+  ${QT_LIBS}
+  Qt6::Test
+)
+
+add_test(scenemousearea_test)
+gtest_discover_tests(scenemousearea_test)
diff --git a/test/scenemousearea/scenemousearea_test.cpp b/test/scenemousearea/scenemousearea_test.cpp
new file mode 100644
index 0000000..554b3ba
--- /dev/null
+++ b/test/scenemousearea/scenemousearea_test.cpp
@@ -0,0 +1,50 @@
+#include <QtTest/QSignalSpy>
+#include <scenemousearea.h>
+#include <projectloader.h>
+#include <renderedtargetmock.h>
+
+#include "../common.h"
+
+using namespace scratchcpprender;
+
+TEST(SceneMouseAreaTest, Constructors)
+{
+    SceneMouseArea area1;
+    SceneMouseArea area2(&area1);
+    ASSERT_EQ(area2.parent(), &area1);
+    ASSERT_EQ(area2.parentItem(), &area1);
+}
+
+TEST(SceneMouseAreaTest, Stage)
+{
+    SceneMouseArea mouseArea;
+    QSignalSpy spy(&mouseArea, &SceneMouseArea::stageChanged);
+    ASSERT_EQ(mouseArea.stage(), nullptr);
+
+    RenderedTargetMock stage;
+    mouseArea.setStage(&stage);
+    ASSERT_EQ(spy.count(), 1);
+    ASSERT_EQ(mouseArea.stage(), &stage);
+}
+
+TEST(SceneMouseAreaTest, ProjectLoader)
+{
+    SceneMouseArea mouseArea;
+    QSignalSpy spy(&mouseArea, &SceneMouseArea::projectLoaderChanged);
+    ASSERT_EQ(mouseArea.projectLoader(), nullptr);
+
+    ProjectLoader loader;
+    mouseArea.setProjectLoader(&loader);
+    ASSERT_EQ(spy.count(), 1);
+    ASSERT_EQ(mouseArea.projectLoader(), &loader);
+}
+
+TEST(SceneMouseAreaTest, DraggedSprite)
+{
+    SceneMouseArea mouseArea;
+    ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+
+    RenderedTargetMock sprite;
+    mouseArea.setDraggedSprite(&sprite);
+    ASSERT_EQ(mouseArea.draggedSprite(), &sprite);
+}
diff --git a/test/svg_result.png b/test/svg_result.png
index 1131518..86dceff 100644
Binary files a/test/svg_result.png and b/test/svg_result.png differ
diff --git a/test/target_models/CMakeLists.txt b/test/target_models/CMakeLists.txt
index 434292e..bf565a2 100644
--- a/test/target_models/CMakeLists.txt
+++ b/test/target_models/CMakeLists.txt
@@ -8,8 +8,8 @@ target_link_libraries(
   stagemodel_test
   GTest::gtest_main
   GTest::gmock_main
-  libscratchcpp-gui
-  scratchcppgui_mocks
+  scratchcpp-render
+  scratchcpprender_mocks
   qnanopainter
   ${QT_LIBS}
   Qt6::Test
@@ -28,8 +28,8 @@ target_link_libraries(
   spritemodel_test
   GTest::gtest_main
   GTest::gmock_main
-  libscratchcpp-gui
-  scratchcppgui_mocks
+  scratchcpp-render
+  scratchcpprender_mocks
   qnanopainter
   ${QT_LIBS}
   Qt6::Test
diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp
index 97a3fab..17e9df2 100644
--- a/test/target_models/spritemodel_test.cpp
+++ b/test/target_models/spritemodel_test.cpp
@@ -5,7 +5,7 @@
 
 #include "../common.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 using namespace libscratchcpp;
 
 TEST(SpriteModelTest, Constructors)
@@ -23,24 +23,59 @@ TEST(SpriteModelTest, Init)
     Sprite sprite;
     model.init(&sprite);
     ASSERT_EQ(model.sprite(), &sprite);
+}
 
-    auto c1 = std::make_shared<Costume>("", "", "");
-    auto c2 = std::make_shared<Costume>("", "", "");
-    auto c3 = std::make_shared<Costume>("", "", "");
-    sprite.addCostume(c1);
-    sprite.addCostume(c2);
-    sprite.addCostume(c3);
-    sprite.setCostumeIndex(1);
-
-    RenderedTargetMock renderedTarget;
-    QSignalSpy spy(&model, &SpriteModel::renderedTargetChanged);
-    EXPECT_CALL(renderedTarget, loadCostume(c2.get()));
-    model.setRenderedTarget(&renderedTarget);
+TEST(SpriteModelTest, DeInitClone)
+{
+    SpriteModel model;
+    QSignalSpy spy(&model, &SpriteModel::cloneDeleted);
+    model.deinitClone();
     ASSERT_EQ(spy.count(), 1);
-    ASSERT_EQ(model.renderedTarget(), &renderedTarget);
+    QList<QVariant> args = spy.takeFirst();
+    ASSERT_EQ(args.size(), 1);
+    SpriteModel *modelPtr = args.at(0).value<SpriteModel *>();
+    ASSERT_EQ(modelPtr, &model);
+}
 
-    EXPECT_CALL(renderedTarget, loadCostume(c2.get()));
-    model.init(&sprite);
+TEST(SpriteModelTest, OnCloned)
+{
+    SpriteModel model;
+
+    Sprite clone1;
+    QSignalSpy spy1(&model, &SpriteModel::cloned);
+    model.onCloned(&clone1);
+    ASSERT_EQ(spy1.count(), 1);
+
+    QList<QVariant> args = spy1.takeFirst();
+    ASSERT_EQ(args.size(), 1);
+    SpriteModel *cloneModel = args.at(0).value<SpriteModel *>();
+    ASSERT_TRUE(cloneModel);
+    ASSERT_EQ(cloneModel->parent(), &model);
+    ASSERT_EQ(cloneModel->sprite(), &clone1);
+    spy1.clear();
+
+    Sprite clone2;
+    model.onCloned(&clone2);
+    ASSERT_EQ(spy1.count(), 1);
+
+    args = spy1.takeFirst();
+    ASSERT_EQ(args.size(), 1);
+    cloneModel = args.at(0).value<SpriteModel *>();
+    ASSERT_TRUE(cloneModel);
+    ASSERT_EQ(cloneModel->parent(), &model);
+    ASSERT_EQ(cloneModel->sprite(), &clone2);
+
+    Sprite clone3;
+    QSignalSpy spy2(cloneModel, &SpriteModel::cloned);
+    cloneModel->onCloned(&clone3);
+    ASSERT_EQ(spy2.count(), 1);
+
+    args = spy2.takeFirst();
+    ASSERT_EQ(args.size(), 1);
+    cloneModel = args.at(0).value<SpriteModel *>();
+    ASSERT_TRUE(cloneModel);
+    ASSERT_EQ(cloneModel->parent(), &model);
+    ASSERT_EQ(cloneModel->sprite(), &clone3);
 }
 
 TEST(SpriteModelTest, OnCostumeChanged)
@@ -56,30 +91,83 @@ TEST(SpriteModelTest, OnCostumeChanged)
     model.onCostumeChanged(&costume);
 }
 
+TEST(SpriteModelTest, OnVisibleChanged)
+{
+    SpriteModel model;
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
+
+    EXPECT_CALL(renderedTarget, updateVisibility(true));
+    model.onVisibleChanged(true);
+
+    EXPECT_CALL(renderedTarget, updateVisibility(false));
+    model.onVisibleChanged(false);
+}
+
+TEST(SpriteModelTest, OnXChanged)
+{
+    SpriteModel model;
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
+
+    EXPECT_CALL(renderedTarget, updateX(32.4));
+    model.onXChanged(32.4);
+}
+
+TEST(SpriteModelTest, OnYChanged)
+{
+    SpriteModel model;
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
+
+    EXPECT_CALL(renderedTarget, updateY(-46.1));
+    model.onYChanged(-46.1);
+}
+
 TEST(SpriteModelTest, OnSizeChanged)
 {
     SpriteModel model;
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
 
-    Costume costume("", "", "");
+    EXPECT_CALL(renderedTarget, updateSize(65.2));
+    model.onSizeChanged(65.2);
+}
 
-    Sprite sprite;
-    model.init(&sprite);
-    ASSERT_EQ(model.sprite(), &sprite);
+TEST(SpriteModelTest, OnDirectionChanged)
+{
+    SpriteModel model;
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
+
+    EXPECT_CALL(renderedTarget, updateDirection(-5.4));
+    model.onDirectionChanged(-5.4);
+}
+
+TEST(SpriteModelTest, OnRotationStyleChanged)
+{
+    SpriteModel model;
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
 
-    auto c1 = std::make_shared<Costume>("", "", "");
-    auto c2 = std::make_shared<Costume>("", "", "");
-    auto c3 = std::make_shared<Costume>("", "", "");
-    sprite.addCostume(c1);
-    sprite.addCostume(c2);
-    sprite.addCostume(c3);
-    sprite.setCostumeIndex(1);
+    EXPECT_CALL(renderedTarget, updateRotationStyle(Sprite::RotationStyle::AllAround));
+    model.onRotationStyleChanged(Sprite::RotationStyle::AllAround);
 
+    EXPECT_CALL(renderedTarget, updateRotationStyle(Sprite::RotationStyle::LeftRight));
+    model.onRotationStyleChanged(Sprite::RotationStyle::LeftRight);
+
+    EXPECT_CALL(renderedTarget, updateRotationStyle(Sprite::RotationStyle::DoNotRotate));
+    model.onRotationStyleChanged(Sprite::RotationStyle::DoNotRotate);
+}
+
+TEST(SpriteModelTest, OnLayerOrderChanged)
+{
+    SpriteModel model;
     RenderedTargetMock renderedTarget;
-    EXPECT_CALL(renderedTarget, loadCostume(c2.get()));
     model.setRenderedTarget(&renderedTarget);
 
-    EXPECT_CALL(renderedTarget, loadCostume(c2.get()));
-    model.onSizeChanged(150);
+    EXPECT_CALL(renderedTarget, updateLayerOrder(7));
+    model.onLayerOrderChanged(7);
 }
 
 TEST(SpriteModelTest, RenderedTarget)
diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp
index 71343a7..822e344 100644
--- a/test/target_models/stagemodel_test.cpp
+++ b/test/target_models/stagemodel_test.cpp
@@ -5,7 +5,7 @@
 
 #include "../common.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 using namespace libscratchcpp;
 
 TEST(StageModelTest, Constructors)
@@ -23,24 +23,6 @@ TEST(StageModelTest, Init)
     Stage stage;
     model.init(&stage);
     ASSERT_EQ(model.stage(), &stage);
-
-    auto c1 = std::make_shared<Costume>("", "", "");
-    auto c2 = std::make_shared<Costume>("", "", "");
-    auto c3 = std::make_shared<Costume>("", "", "");
-    stage.addCostume(c1);
-    stage.addCostume(c2);
-    stage.addCostume(c3);
-    stage.setCostumeIndex(1);
-
-    RenderedTargetMock renderedTarget;
-    QSignalSpy spy(&model, &StageModel::renderedTargetChanged);
-    EXPECT_CALL(renderedTarget, loadCostume(c2.get()));
-    model.setRenderedTarget(&renderedTarget);
-    ASSERT_EQ(spy.count(), 1);
-    ASSERT_EQ(model.renderedTarget(), &renderedTarget);
-
-    EXPECT_CALL(renderedTarget, loadCostume(c2.get()));
-    model.init(&stage);
 }
 
 TEST(StageModelTest, OnCostumeChanged)
@@ -60,10 +42,25 @@ TEST(StageModelTest, RenderedTarget)
 {
     StageModel model;
     ASSERT_EQ(model.renderedTarget(), nullptr);
+    Stage stage;
+    model.init(&stage);
+
+    auto c1 = std::make_shared<Costume>("", "", "");
+    auto c2 = std::make_shared<Costume>("", "", "");
+    auto c3 = std::make_shared<Costume>("", "", "");
+    stage.addCostume(c1);
+    stage.addCostume(c2);
+    stage.addCostume(c3);
+    stage.setCostumeIndex(1);
 
     RenderedTargetMock renderedTarget;
     QSignalSpy spy(&model, &StageModel::renderedTargetChanged);
+    EXPECT_CALL(renderedTarget, loadCostume(c2.get()));
     model.setRenderedTarget(&renderedTarget);
     ASSERT_EQ(spy.count(), 1);
     ASSERT_EQ(model.renderedTarget(), &renderedTarget);
+
+    EXPECT_CALL(renderedTarget, loadCostume(c3.get()));
+    stage.setCostumeIndex(2);
+    model.loadCostume();
 }
diff --git a/test/targetpainter/CMakeLists.txt b/test/targetpainter/CMakeLists.txt
index f632ac7..bdf9793 100644
--- a/test/targetpainter/CMakeLists.txt
+++ b/test/targetpainter/CMakeLists.txt
@@ -7,8 +7,8 @@ target_link_libraries(
   targetpainter_test
   GTest::gtest_main
   GTest::gmock_main
-  libscratchcpp-gui
-  scratchcppgui_mocks
+  scratchcpp-render
+  scratchcpprender_mocks
   ${QT_LIBS}
   qnanopainter
 )
diff --git a/test/targetpainter/targetpainter_test.cpp b/test/targetpainter/targetpainter_test.cpp
index 6d6acbd..842b9bc 100644
--- a/test/targetpainter/targetpainter_test.cpp
+++ b/test/targetpainter/targetpainter_test.cpp
@@ -3,7 +3,7 @@
 
 #include "../common.h"
 
-using namespace scratchcppgui;
+using namespace scratchcpprender;
 
 using ::testing::Return;
 using ::testing::ReturnRef;
@@ -57,7 +57,7 @@ TEST_F(TargetPainterTest, PaintBitmap)
 
         // Begin painting reference
         QNanoPainter refPainter;
-        QOpenGLFramebufferObject refFbo(100, 100, format);
+        QOpenGLFramebufferObject refFbo(40, 60, format);
         refFbo.bind();
         refPainter.beginFrame(refFbo.width(), refFbo.height());
 
@@ -67,7 +67,7 @@ TEST_F(TargetPainterTest, PaintBitmap)
         refPainter.endFrame();
 
         // Begin painting
-        QOpenGLFramebufferObject fbo(100, 100, format);
+        QOpenGLFramebufferObject fbo(40, 60, format);
         fbo.bind();
         painter.beginFrame(fbo.width(), fbo.height());
 
@@ -79,6 +79,7 @@ TEST_F(TargetPainterTest, PaintBitmap)
         EXPECT_CALL(target, bitmapBuffer()).WillOnce(Return(&buffer));
         static const QString uniqueKey("abc");
         EXPECT_CALL(target, bitmapUniqueKey()).WillOnce(ReturnRef(uniqueKey));
+        EXPECT_CALL(target, updateHullPoints);
         EXPECT_CALL(target, unlockCostume());
         targetPainter.paint(&painter);
         painter.endFrame();
@@ -110,6 +111,7 @@ TEST_F(TargetPainterTest, PaintSvg)
     EXPECT_CALL(target, height()).WillOnce(Return(60));
     EXPECT_CALL(target, isSvg()).WillOnce(Return(true));
     EXPECT_CALL(target, paintSvg(&painter));
+    EXPECT_CALL(target, updateHullPoints);
     EXPECT_CALL(target, unlockCostume());
     targetPainter.paint(&painter);
 
diff --git a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp
index 170e344..a2041dd 100644
--- a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp
+++ b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp
@@ -154,6 +154,7 @@ QOpenGLFramebufferObject *QNanoQuickItemPainter::createFramebufferObject(const Q
 {
     QOpenGLFramebufferObjectFormat format;
     format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
+    format.setSamples(16);
     QSize fboSize(size);
     if (m_textureWidth > -1) fboSize.setWidth(static_cast<int>(m_textureWidth*m_itemData.devicePixelRatio));
     if (m_textureHeight > -1) fboSize.setHeight(static_cast<int>(m_textureHeight*m_itemData.devicePixelRatio));
