diff --git a/src/simudator/gui/dialogs/module_state_dialog.py b/src/simudator/gui/dialogs/module_state_dialog.py
index 622112d17d1d88ac6ec09483cd4b13e0d4a95c00..fc768d7ade04490453e641d1f77fc3e2d5a76266 100644
--- a/src/simudator/gui/dialogs/module_state_dialog.py
+++ b/src/simudator/gui/dialogs/module_state_dialog.py
@@ -1,7 +1,6 @@
-from typing import Optional
-
 from qtpy.QtCore import Qt
 from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
 from qtpy.QtWidgets import (
     QComboBox,
     QDialog,
@@ -12,65 +11,97 @@ from qtpy.QtWidgets import (
 )
 
 from simudator.core import Module
+from simudator.gui.formatting import format_to_str, parse_str
+from simudator.gui.module_graphics_item.module_widget import ModuleWidget
 
 
 class ModuleStateDialog(QDialog):
-    accepted = pyqtSignal(str, str, str, name='okSignal')
+    """
+    A dialog for letting the user edit the state of a processor module.
+
+    Parameters
+    ----------
+    module : Module
+        The module to edit the state of.
+    module_widget : ModuleWidget
+        The corresponding module widget for the module. Used mainly for
+        formatting and parsing the values of the state variables of the module.
+    parent : QWidget | None
+        Optional parent widget of this dialog.
+    flags : Qt.WindowFlags | Qt.WindowType
+        Optional window flags for the window of the dialog.
+    """
+
+    state_edited = pyqtSignal()
 
     def __init__(
         self,
         module: Module,
-        parent: Optional['QWidget'] = None,
+        module_widget: ModuleWidget,
+        parent: QWidget | None = None,
         flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
     ) -> None:
         super().__init__(parent, flags)
 
-        self.module = module
-        states = module.get_gui_state()
-
-        # Remove the 'name' state of the module from the drop down menu so
-        # that the user cannot e.g. edit or add a breakpoint of a module
-        states.pop('name')
+        self._module = module
+        self._state_vars = module_widget.get_state_vars()
 
-        # Set up the drop down menu for selecting a state of the module
-        # to perform an action to
-        self.stateSelectWidget = QComboBox()
-        self.stateSelectWidget.addItems(states.keys())
-        self.stateSelectWidget.activated.connect(self.updateState)
+        # Set up the drop down menu for selecting a state variable of the module
+        self._state_var_widget = QComboBox()
+        self._state_var_widget.addItems(self._state_vars.keys())
 
-        # Set up a text field for entering the value used for the e.g. editing
-        # the selected state
-        self.valueWidget = QLineEdit()
+        # Set up a text field for displaying and editing the value of the
+        # selected state variable
+        self._value_widget = QLineEdit()
 
         # Set up buttons
-        self.okButton = QPushButton('OK')
-        self.okButton.clicked.connect(self.signalOK)
-        self.cancelButton = QPushButton('Cancel')
-        self.cancelButton.clicked.connect(self.close)
+        self._accept_button = QPushButton('OK')
+        self._accept_button.clicked.connect(self.edit_state)
+        self._cancel_button = QPushButton('Cancel')
+        self._cancel_button.clicked.connect(self.close)
 
         # Set up the layout of the widget
-        self.HBoxLayout = QHBoxLayout()
-        self.HBoxLayout.addWidget(self.stateSelectWidget, 0)
-        self.HBoxLayout.addWidget(self.valueWidget, 1)
-        self.HBoxLayout.addWidget(self.okButton, 2)
-        self.HBoxLayout.addWidget(self.cancelButton, 3)
-        self.setLayout(self.HBoxLayout)
+        self._layout = QHBoxLayout()
+        self._layout.addWidget(self._state_var_widget, 0)
+        self._layout.addWidget(self._value_widget, 1)
+        self._layout.addWidget(self._accept_button, 2)
+        self._layout.addWidget(self._cancel_button, 3)
+        self.setLayout(self._layout)
 
         # Show the widget
-        self.updateState()
-        self.show()
-
-    def updateState(self) -> None:
-        selectedState = self.stateSelectWidget.currentText()
-        value = self.module.get_gui_state()[selectedState]
-        self.valueWidget.setText(str(value))
-
-    def signalOK(self) -> None:
-        module_name = self.module.get_state()['name']
-        selectedState = self.stateSelectWidget.currentText()
-        enteredValue = self.valueWidget.text()
-        self.accepted.emit(module_name, selectedState, enteredValue)
-        self.close()
-
-    def close(self) -> bool:
-        return super().close()
+        self.select_state_var()
+        self.exec()
+
+    @Slot()
+    def select_state_var(self) -> None:
+        """
+        Select a state variable of the module to edit.
+        """
+        selected_state = self._state_var_widget.currentText()
+        format_info = self._state_vars[selected_state]
+        value = format_to_str(format_info, self._module.get_state()[selected_state])
+        self._value_widget.setText(value)
+
+    @Slot()
+    def edit_state(self) -> None:
+        """
+        Parse the user input and edit the state of the module associated with
+        this dialog.
+
+        This triggers the `state_edited` signal if the parsing is
+        successful. Else, the user is given an error message.
+        """
+        state = self._module.get_state()
+        selected_state_var = self._state_var_widget.currentText()
+        try:
+            value = parse_str(
+                self._state_vars[selected_state_var], self._value_widget.text()
+            )
+            state[selected_state_var] = value
+            self._module.set_state(state)
+            self.state_edited.emit()
+            self.close()
+        except ValueError as e:
+            # TODO: error handling
+            pass
+
diff --git a/src/simudator/gui/formatting.py b/src/simudator/gui/formatting.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed57c8f4db9c84547663a82516e3af8da8a5167d
--- /dev/null
+++ b/src/simudator/gui/formatting.py
@@ -0,0 +1,118 @@
+import math
+from enum import Enum, auto
+from typing import Any
+
+
+class Format(Enum):
+    """
+    An Enum class containing the different valid formats for formatting and
+    parsing values of different data types.
+    """
+
+    Str = (auto(),)
+    Int = (auto(),)
+    Bin = (auto(),)
+    Hex = (auto(),)
+    Decimal = (auto(),)
+    DecimalSigned = (auto(),)
+
+
+class FormatInfo:
+    """
+    A class for neatly keeping data about the supported formats and selected
+    format for correctly displaying and parsing values of different data types.
+
+    Parameters
+    ----------
+    supported_format : list[Format]
+        List of formats that are supported for some variable.
+    selected_format : Format
+        The format that should be used on initialization.
+    **kwargs
+        Arguments passed to `format_to_str` and `parse_str`. Required by some
+        formats.
+
+    Attributes
+    ----------
+    supported_formats : list[Format]
+        List of format that are supported.
+    selected_format : Format
+        The selected format to use when formatting/parsing. Should be one of
+        the supported formats.
+    """
+
+    NUMERICAL_FORMATS = (
+        Format.Int,
+        Format.Bin,
+        Format.Hex,
+        Format.Decimal,
+        Format.DecimalSigned,
+    )
+    """
+    List of standard numerical formats for handling numbers. It is possible to
+    switch between these formats easily as they all require the same
+    keyword arguments.
+    """
+
+    __slots__ = ("selected_format", "supported_formats", "kwargs")
+
+    def __init__(
+        self, supported_formats: list[Format], selected_format: Format, **kwargs
+    ) -> None:
+        self.selected_format = selected_format
+        self.supported_formats = supported_formats
+        self.kwargs = kwargs
+
+
+def format_to_str(format_info: FormatInfo, value: Any) -> str:
+    """
+    Create a formatted string from a given value.
+
+    Parameters
+    ----------
+    format_info : FormatInfo
+        A class containing information about what format to use.
+    value : Any
+        An arbitrary value that is to be formatted to a string.
+
+    Returns
+    -------
+    The input value formatted to a string according to the input variable
+    format.
+    """
+    match format_info.selected_format:
+        case Format.Int:
+            pass
+        case Format.Str:
+            pass
+        case Format.Bin:
+            return f"{value:0{format_info.kwargs['bit_length']}b}"
+        case Format.Hex:
+            return f"{value:0{math.ceil(format_info.kwargs['bit_length']/4)}x}"
+        case Format.Decimal:
+            pass
+        case Format.DecimalSigned:
+            pass
+
+
+def parse_str(format_info: FormatInfo, value: str) -> Any:
+    """
+    Parse an input string formatted using the given format.
+
+    Parameters
+    ----------
+    format_info : FormatInfo
+        An instance containing information about the format used on the string.
+    value : str
+        A formatted string to parse into a value.
+
+    Returns
+    -------
+    The parsed value from the formatted string. The data type of the value
+    depends on the format.
+    """
+    match format_info.selected_format:
+        case Format.Int:
+            return int(value)
+        case Format.Hex:
+            return int(value, base=16)
diff --git a/src/simudator/gui/gui.py b/src/simudator/gui/gui.py
index 9b3fdc9cb05e293a70869cc1c078057c23668475..63c39edcea6018b47973b3e2fa9edb5997e1d0e6 100644
--- a/src/simudator/gui/gui.py
+++ b/src/simudator/gui/gui.py
@@ -2,7 +2,6 @@ import ast
 import sys
 
 from qtpy import QtCore, QtWidgets
-from qtpy.QtCore import Signal as pyqtSignal
 from qtpy.QtCore import Slot
 from qtpy.QtWidgets import (
     QAction,
@@ -19,6 +18,7 @@ from simudator.gui.cpu_graphics_scene import CpuGraphicsScene
 from simudator.gui.dialogs.lambda_breakpoint_dialog import LambdaBreakpointDialog
 from simudator.gui.menu_bar import MainMenuBar
 from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem
+from simudator.gui.module_graphics_item.module_widget import ModuleWidget
 from simudator.gui.pipeline import PipeLine
 from simudator.gui.processor_handler import ProcessorHandler
 from simudator.gui.signal_viewer import SignalViewer
@@ -182,12 +182,6 @@ class GUI(QMainWindow):
         if self._breakpoint_window is not None:
             self._breakpoint_window.update()
 
-    """
-    @Slot is used to explicitly mark a python method as a Qt slot
-    and specify a C++ signature for it, which is used most commonly
-    in order to select a particular overload.
-    """
-
     @Slot(str, str, str)
     def editModuleState(self, module_name, state, value) -> None:
         """
@@ -297,6 +291,20 @@ class GUI(QMainWindow):
         self.connectModuleActions(item.getActionSignals())
         self._processor_handler.changed.connect(item.update)
 
+    def add_module_widget(self, widget: ModuleWidget) -> None:
+        """
+        Add a module widget to the processor scene and connect its QT signals
+        and slots.
+
+        Parameters
+        ----------
+        widget : ModuleWidget
+        The module widget to add to the processor scene.
+        """
+        self._processor_handler.changed.connect(widget.update)
+        self._graphics_scene.addItem(widget)
+        widget.state_changed.connect(self._processor_handler.handle_module_change)
+
     def add_all_signals(self) -> None:
         """
         Add visual representations of all processor signals between all modules
diff --git a/src/simudator/gui/module_graphics_item/actions.py b/src/simudator/gui/module_graphics_item/actions.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1abb1f5f37d1f974e2816e03fe39a8af5e1a6c8
--- /dev/null
+++ b/src/simudator/gui/module_graphics_item/actions.py
@@ -0,0 +1,67 @@
+from enum import Enum, auto
+
+from PyQt5.QtWidgets import QAction
+
+from simudator.core.module import Module
+from simudator.gui.dialogs.module_state_dialog import ModuleStateDialog
+from simudator.gui.module_graphics_item.module_widget import ModuleWidget
+
+
+class ActionType(Enum):
+    """
+    An Enum class containing the different valid actions for interacting with
+    module widgets.
+    """
+
+    EditState = (auto(),)
+
+
+def edit_state_action(module: Module, module_widget: ModuleWidget) -> QAction:
+    """
+    Create a QAction for opening a state-edit dialog for a module widget.
+    The dialog triggers the state_changed signal of the module widget if the
+    user accepts the dialog.
+
+    Parameters
+    ----------
+    module : Module
+        The module that should be edited when triggering the created edit action.
+    module_widget : ModuleWidget
+        The module widget for the module that should be edited. Used for
+        displaying and parsing information correctly.
+
+    Returns
+    -------
+    QAction
+        A Qt action for opening a dialog for editing the state of a module.
+        Intended to be added as an action to the given module widget.
+    """
+    action = QAction("Edit state", module_widget)
+
+    def action_triggered():
+        dialog = ModuleStateDialog(module, module_widget)
+        dialog.state_edited.connect(module_widget.state_changed)
+
+    action.triggered.connect(action_triggered)
+    return action
+
+
+def toggle_ports_action(module_widget: ModuleWidget) -> QAction:
+    """
+    Create a QAction for toggling the visibility of port widgets of a module
+    widget.
+
+    Parameters
+    ----------
+    module_widget : ModuleWidget
+        The module for which its ports should be toggled to visible or invisible
+        using a QAction.
+
+    Returns
+    -------
+        QAction for toggling port visibility of a module widget.
+    """
+    action = QAction("Toggle ports", module_widget)
+    action.triggered.connect(module_widget.toggle_ports)
+    return action
+
diff --git a/src/simudator/gui/module_graphics_item/module_widget.py b/src/simudator/gui/module_graphics_item/module_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..53bfaf40c443c01069d313908f2fc630c89b69df
--- /dev/null
+++ b/src/simudator/gui/module_graphics_item/module_widget.py
@@ -0,0 +1,337 @@
+from qtpy.QtCore import Property, QRectF, Qt
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtGui import QBrush, QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen
+from qtpy.QtWidgets import (
+    QErrorMessage,
+    QGraphicsItem,
+    QGraphicsSceneContextMenuEvent,
+    QGraphicsWidget,
+    QMenu,
+    QStyleOptionGraphicsItem,
+    QWidget,
+)
+
+from simudator.core.module import Module
+from simudator.gui.formatting import Format, FormatInfo, format_to_str
+from simudator.gui.port_widget import PortWidget
+
+
+class ModuleWidget(QGraphicsWidget):
+    """
+    A general class for displaying an arbitrary processor module. The
+    appearance of a module can be customized by changing its properties and
+    by choosing which state variables to display and in what formats.
+
+    The default appearance is a 150x150 rectangle with an outline of width 1
+    and inner padding set to 5. Text id by default displayed in Sans Serif 9pt.
+    The default brushes for outline, text and background are the ones provided
+    by the palette of the widget.
+
+    Parameters
+    ----------
+    module : Module
+        The processor module to display.
+    state_vars : dict[str, FormatInfo]
+        The state variables of the module to display and in what formats to
+        display them.
+    ports: list[str]
+        List of names for signal port widgets to create and display for this
+        module widget.
+    parent : QGraphicsItem | None
+        Optional parent item of this module widget.
+    flags : Qt.WindowFlags | Qt.WindowType
+        Optional window flags for the window of this widget.
+    """
+
+    # Used for controlling the default appearance. Make sure these are
+    # consistent with the class docstring
+    _DEFAULT_WIDTH = 150
+    _DEFAULT_HEIGHT = 150
+    _DEFAULT_OUTLINE_WIDTH = 1
+    _DEFAULT_TEXT_FONT = QFont("Sans Serif", 9)
+    _DEFAULT_PADDING = 5
+
+    state_changed = pyqtSignal()
+
+    def __init__(
+        self,
+        module: Module,
+        state_vars: dict[str, FormatInfo],
+        ports: list[str],
+        parent: QGraphicsItem | None = None,
+        flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
+    ) -> None:
+        super().__init__(parent, flags)
+        super().__init__(parent, flags)
+        self.setFlag(QGraphicsItem.ItemIsMovable)
+
+        self._module = module
+        self._state_vars = state_vars
+        self._port_widgets: list[PortWidget] = []
+
+        # Default values for properties for appearance
+        self._outline_width = self._DEFAULT_OUTLINE_WIDTH
+        self._outline = self.palette().windowText()
+        self._background = self.palette().window()
+        self._text_color = self.palette().windowText().color()
+        self._text_font = self._DEFAULT_TEXT_FONT
+        self._padding = self._DEFAULT_PADDING
+
+        # Set the size of the module depending on the number of state variables
+        width = self._DEFAULT_WIDTH
+        height = self._DEFAULT_HEIGHT
+        if len(self._state_vars) <= 1:
+            height = QFontMetrics(self._text_font).height() + 2 * self._padding
+        else:
+            height = (self._padding + QFontMetrics(self._text_font).height()) * (
+                len(state_vars) + 1
+            ) + self._padding
+        self.resize(width, height)
+
+        self._create_ports(ports)
+
+    def paint(
+        self,
+        painter: QPainter | None = None,
+        option: QStyleOptionGraphicsItem | None = None,
+        widget: QWidget | None = None,
+    ) -> None:
+        painter.save()
+
+        width = self.size().width()
+        height = self.size().height()
+
+        # Draw the base shape
+        pen = QPen(self.outline, self.outline_width)
+        painter.setPen(pen)
+        painter.setBrush(self.background)
+        painter.drawPath(self.shape())
+
+        # Set text specific painter settings
+        painter.setPen(QPen(self.text_color))
+        painter.setBrush(QBrush(self.text_color))
+        painter.setFont(self.text_font)
+        text_flags = Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap
+
+        text_rect = self.shape().boundingRect()
+
+        # Draw only name of the module if no state variables are to be shown
+        if len(self._state_vars.keys()) == 0:
+            painter.drawText(text_rect, text_flags, self._module.get_state()["name"])
+
+        # Special case: only 1 state variable to show
+        elif len(self._state_vars.keys()) == 1:
+            state_var = list(self._state_vars.keys())[0]
+            format_info = self._state_vars[state_var]
+            value = format_to_str(format_info, self._module.get_state()[state_var])
+            painter.drawText(
+                text_rect, text_flags, f"{self._module.get_state()['name']}: {value}"
+            )
+
+        # Draw the name of the module and each state variable with its value on one row each
+        else:
+            text_rect = QRectF(0, 0, width, height / (len(self._state_vars) + 1))
+            painter.drawText(text_rect, text_flags, self._module.get_state()["name"])
+
+            for i, state_var in enumerate(self._state_vars):
+                y_start = height / (len(self._state_vars) + 1) * (i + 1)
+                y_end = height / (len(self._state_vars) + 1)
+                text_rect = QRectF(0, y_start, width, y_end)
+
+                format_info = self._state_vars[state_var]
+                value = format_to_str(format_info, self._module.get_state()[state_var])
+                painter.drawText(text_rect, text_flags, f"{state_var}: {value}")
+
+        painter.restore()
+
+    @Slot()
+    def update(self, rect: QRectF | None = None):
+        # This "override" is needed in order to decorate update() as a
+        # pyqt slot
+        if rect is None:
+            rect = self.boundingRect()
+        super().update(rect)
+
+    def shape(self) -> QPainterPath:
+        path = QPainterPath()
+        path.addRect(0, 0, self.size().width(), self.size().height())
+        return path
+
+    def boundingRect(self) -> QRectF:
+        width = self.size().width()
+        height = self.size().height()
+        margin = self.outline_width / 2
+        return QRectF(0 - margin, 0 - margin, width + 2 * margin, height + 2 * margin)
+
+    def _create_ports(self, ports: list[str]) -> None:
+        """
+        Instantiate and add port widgets to this module widget.
+
+        Parameters
+        ----------
+        ports : list[str]
+            List of names for the ports to create. One widget is created per
+            each name.
+        """
+        for port_name in ports:
+            port = PortWidget(port_name, parent=self)
+            port.setPos(0, 0)
+            self._port_widgets.append(port)
+
+    @Slot()
+    def toggle_ports(self) -> None:
+        """
+        Toggle the visibility of the signal ports of the displayed module.
+        """
+        for port in self._port_widgets:
+            port.setVisible(not port.isVisible())
+
+    def contextMenuEvent(self, event: 'QGraphicsSceneContextMenuEvent') -> None:
+        """
+        Show a context menu for this module, allowing the user to perform any
+        of the actions available to this module.
+        """
+        menu = QMenu()
+        menu.addActions(self.actions())
+        menu.exec_(event.screenPos())
+
+    def get_state_vars(self) -> dict[str, FormatInfo]:
+        """
+        Return a mapping from displayed state variables of the displayed
+        module to their respective format information.
+
+        Useful for viewing the supported formats and the selected format for a
+        state variable.
+
+        Returns
+        -------
+        dict[str, FormatInfo]
+            Mapping from state variable to FormatInfo instance.
+        """
+        return self._state_vars
+
+    @Slot()
+    def set_format(self, state_var: str, format: Format) -> None:
+        """
+        Set the format to be used when displaying and parsing a given state
+        variable of the displayed module.
+
+        Parameters
+        ----------
+        state_va : str
+            The state variable to set the formats of.
+        format : Format
+            The format to use for the value of the state variable. Should be
+            one of the supported formats in the FormatInfo instance for the
+            state variable.
+        """
+        self._state_vars[state_var].selected_format = format
+        self.update()
+
+    def outline_width(self) -> float:
+        """Return the outline pen width.
+
+        Returns
+        -------
+        float
+            Outline pen width.
+        """
+        return self._outline_width
+
+    def set_outline_width(self, width: float) -> None:
+        """Set the outline pen width.
+
+        Parameters
+        ----------
+        width : float
+            Outline pen width.
+
+        """
+        self._outline_width = width
+
+    def outline(self) -> QBrush:
+        """Return the outline brush used to create the outline pen.
+
+        Returns
+        -------
+        QBrush
+            Outline brush.
+        """
+        return self._outline
+
+    def set_outline(self, brush: QBrush) -> None:
+        """Set the outline brush used to create the outline pen.
+
+        Parameters
+        ----------
+        brush : QBrush
+            Outline brush.
+        """
+        self._outline = brush
+
+    def background(self) -> QBrush:
+        """Return the bursh used for filling the background.
+
+        Returns
+        -------
+        QBrush
+            Background brush.
+        """
+        return self._background
+
+    def set_background(self, brush: QBrush) -> None:
+        """Set the bursh used for filling the background.
+
+        Parameters
+        ----------
+        brush : QBrush
+            Background brush.
+        """
+        self._background = brush
+
+    def text_color(self) -> QColor:
+        """Return the color used for text.
+
+        Returns
+        -------
+        QColor
+            Text color.
+        """
+        return self._text_color
+
+    def set_text_color(self, color: QColor) -> None:
+        """Set the color used for text.
+
+        Parameters
+        ----------
+        color : QColor
+            Text color.
+        """
+        self._text_color = color
+
+    def text_font(self) -> QFont:
+        """Return the font used for text.
+
+        Returns
+        -------
+        QFont
+            Text font.
+        """
+        return self._text_font
+
+    def set_text_font(self, font: QFont) -> None:
+        """Set the font used for text.
+
+        Parameters
+        ----------
+        font : QFont
+            Text font.
+        """
+        self._text_font = font
+
+    outline_width = Property(float, outline_width, set_outline_width)
+    outline = Property(QBrush, outline, set_outline)
+    background = Property(QBrush, background, set_background)
+    text_color = Property(QColor, text_color, set_text_color)
+    text_font = Property(QFont, text_font, set_text_font)
diff --git a/src/simudator/gui/orientation.py b/src/simudator/gui/orientation.py
index 6d3adfe80bf67dbc4b230128b90677109d7bd017..f9d49487870b74746956e9da662304f415700c52 100644
--- a/src/simudator/gui/orientation.py
+++ b/src/simudator/gui/orientation.py
@@ -1,6 +1,7 @@
 from enum import IntEnum
 
 
+# TODO: Remove this file
 class Orientation(IntEnum):
     """
     Used to give and track ports orientations.
diff --git a/src/simudator/gui/port_widget.py b/src/simudator/gui/port_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..b35555f3a0de8563b985d8eff0dde463d2c36740
--- /dev/null
+++ b/src/simudator/gui/port_widget.py
@@ -0,0 +1,399 @@
+from enum import IntEnum
+
+from qtpy.QtCore import Property, QPointF, QRectF, Qt
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtGui import QBrush, QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen
+from qtpy.QtWidgets import (
+    QAction,
+    QGraphicsItem,
+    QGraphicsSceneContextMenuEvent,
+    QGraphicsSceneMouseEvent,
+    QGraphicsWidget,
+    QMenu,
+    QStyleOptionGraphicsItem,
+    QWidget,
+)
+
+
+class Orientation(IntEnum):
+    """
+    Used to give and track ports orientations.
+    """
+
+    UP = 0
+    LEFT = 1
+    DOWN = 2
+    RIGHT = 3
+
+
+class PortWidget(QGraphicsWidget):
+    """
+    A graphics widget for displaying a signal port of a module widget.
+
+    The port is drawn as a line at the edge of the shape of a module widget.
+    A name of the port can also be displayed at the end of the port.
+
+    The default appearance of the port has line length 12, line width 1 and
+    uses the font Sans Serif 9pt.
+
+    Parameters
+    ----------
+    name : str
+        The name to display on the label for the port widget.
+    parent : QGraphicsItem | None
+        Optional parent item of this module widget.
+    flags : Qt.WindowFlags | Qt.WindowType
+        Optional window flags for the window of this widget.
+    """
+
+    _DEFAULT_LINE_LENGTH = 12
+    """
+    Default length of the line that represents this signal port.
+    """
+    _DEFAULT_LINE_WIDTH = 1
+    """
+    Default width of the line that represents this signal port.
+    """
+    _DEFAULT_TEXT_FONT = QFont("Sans Serif", 10)
+    """
+    Default text font used to display the name label of this signal port.
+    """
+
+    signal_toggled = pyqtSignal()
+
+    def __init__(
+        self,
+        name: str,
+        parent: QGraphicsItem | None = None,
+        flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
+    ) -> None:
+        super().__init__(parent, flags)
+        self._name = name
+        # TODO: Change this? Use shape() instead?
+        self._attached_shape = parent.boundingRect()
+
+        self.set_orientation(Orientation.LEFT)
+        self.set_line_length(self._DEFAULT_LINE_LENGTH)
+        self.set_line_width(self._DEFAULT_LINE_WIDTH)
+        self.set_line_brush(self.palette().text())
+        self.set_label_visible(True)
+        self.set_text_font(self._DEFAULT_TEXT_FONT)
+
+        self.setFlag(QGraphicsItem.ItemIsMovable)
+
+    def paint(
+        self,
+        painter: QPainter | None,
+        option: QStyleOptionGraphicsItem | None = None,
+        widget: QWidget | None = None,
+    ) -> None:
+        painter.save()
+
+        # Set style and draw a line to represent the port
+        painter.setPen(QPen(self.line_brush, self.line_width))
+        painter.drawLine(QPointF(0, 0), self.get_end_point())
+
+        # Draw the port label if it should be visible
+        if self.label_visible:
+            # Set text style
+            painter.setFont(self.text_font)
+
+            # Draw text
+            text_rect = self._get_label_rect()
+            painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, self._name)
+
+        painter.restore()
+
+    def get_name(self) -> str:
+        """
+        Return the displayed name of this port.
+
+        Returns
+        -------
+        str
+            Name of this port.
+        """
+        return self._name
+
+    def get_end_point(self) -> QPointF:
+        """
+        Return the point at the end of the line of this port. The end is the one
+        that is not attached to the shape of the module widget that the port
+        is attached to.
+
+        Returns
+        -------
+        QPointF
+            The end point of the port line.
+        """
+        match self.orientation:
+            case Orientation.UP:
+                return QPointF(0, -self.line_length)
+            case Orientation.DOWN:
+                return QPointF(0, self.line_length)
+            case Orientation.LEFT:
+                return QPointF(-self.line_length, 0)
+            case Orientation.RIGHT:
+                return QPointF(self.line_length, 0)
+
+    def _get_label_rect(self) -> QRectF:
+        """
+        Calculate and return a rectangle in which the name label of this port
+        should be placed.
+
+        Returns
+        -------
+        QRectF
+            Rectangle to put the port name label in.
+        """
+        end_point = self.get_end_point()
+        font_metrics = QFontMetrics(self.text_font)
+        text_width = font_metrics.width(self._name)
+        text_height = font_metrics.height()
+
+        match self.orientation:
+            case Orientation.UP:
+                x = end_point.x() - text_width / 2
+                y = end_point.y() - text_height
+            case Orientation.DOWN:
+                x = end_point.x() - text_width / 2
+                y = end_point.y()
+            case Orientation.LEFT:
+                x = end_point.x() - text_width
+                y = end_point.y() - text_height
+            case Orientation.RIGHT:
+                x = end_point.x()
+                y = end_point.y() - text_height
+
+        return QRectF(x, y, text_width, text_height)
+
+    def shape(self) -> QPainterPath:
+        path = QPainterPath()
+        path.addRect(self.boundingRect())
+        return path
+
+    def boundingRect(self) -> QRectF:
+        label_rect = self._get_label_rect()
+        match self.orientation:
+            case Orientation.UP:
+                top_left = label_rect.topLeft()
+                width = label_rect.width()
+                height = label_rect.height() + self.line_length
+
+            case Orientation.DOWN:
+                top_left = label_rect.topLeft() - QPointF(0, self.line_length)
+                width = label_rect.width()
+                height = label_rect.height() + self.line_length
+
+            case Orientation.LEFT:
+                top_left = label_rect.topLeft()
+                width = label_rect.width() + self.line_length
+                height = label_rect.height()
+
+            case Orientation.RIGHT:
+                top_left = QPointF(0, label_rect.top())
+                width = label_rect.width() + self.line_length
+                height = label_rect.height()
+
+        # Add small margin since the line is drawn on the edge of the bounding box.
+        # This removes any possibilities of artifacts appearing.
+        margin = self.line_width / 2
+        return QRectF(
+            top_left.x() - margin,
+            top_left.y() - margin,
+            width + 2 * margin,
+            height + 2 * margin,
+        )
+
+    def contextMenuEvent(
+        self, event: QGraphicsSceneContextMenuEvent | None = None
+    ) -> None:
+        menu = QMenu()
+        hide_signal_action = QAction("Toggle signal visibility", menu)
+        hide_signal_action.triggered.connect(self.signal_toggled)
+        # hide_signal_action.setEnabled(not self.isEnabled)
+
+        hide_port_action = QAction("Toggle port visibility", menu)
+        hide_port_action.triggered.connect(
+            lambda: self.setVisible(not self.isVisible())
+        )
+        # hide_port_action.setEnabled(not self.isLocked)
+
+        menu.addAction(hide_signal_action)
+        menu.addAction(hide_port_action)
+        menu.exec_(event.screenPos())
+        event.accept()
+
+    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
+        if event.buttons() != Qt.MouseButton.LeftButton:
+            return
+
+        rel_x = event.scenePos().x() - self.parentItem().x()
+        rel_y = event.scenePos().y() - self.parentItem().y()
+
+        min_x = self._attached_shape.topLeft().x()
+        min_y = self._attached_shape.topLeft().y()
+        max_x = self._attached_shape.bottomRight().x()
+        max_y = self._attached_shape.bottomRight().y()
+
+        # clamp position between the min and max allowed values
+        x_pos = min(max(rel_x, min_x), max_x)
+        y_pos = min(max(rel_y, min_y), max_y)
+
+        # Make sure the port cannot be placed inside of the module that it
+        # is attached to
+        if not (min_x < x_pos < max_x) or not (min_y < y_pos < max_y):
+            self.setX(x_pos)
+            self.setY(y_pos)
+
+            if x_pos == max_x:
+                self.set_orientation(Orientation.RIGHT)
+            elif x_pos == min_x:
+                self.set_orientation(Orientation.LEFT)
+            elif y_pos == max_y:
+                self.set_orientation(Orientation.DOWN)
+            elif y_pos == min_y:
+                self.set_orientation(Orientation.UP)
+            self.update(self.boundingRect())
+
+    def line_length(self) -> float:
+        """
+        Return the length of the port line.
+
+        Returns
+        -------
+        float
+            The length of the port line as a float.
+        """
+        return self._line_length
+
+    def set_line_length(self, length: float) -> None:
+        """
+        Set the length of the port line.
+
+        Parameters
+        ----------
+        length: float
+            The desired port length.
+        """
+        self._line_length = length
+
+    def line_width(self) -> float:
+        """
+        Return the width of the port line.
+
+        Returns
+        -------
+        float
+            Port line width.
+        """
+        return self._line_width
+
+    def set_line_width(self, width: float) -> None:
+        """
+        Set the width of the port line.
+
+        Parameters
+        ----------
+        length: float
+            The desired port line width.
+        """
+        self._line_width = width
+
+    def line_brush(self) -> QBrush:
+        """
+        Return the brush used to draw the port widget.
+
+        Returns
+        -------
+        QBrush
+            The brush used to draw the port widget.
+        """
+        return self._line_brush
+
+    def set_line_brush(self, brush: QBrush) -> None:
+        """
+        Set the brush used to draw the port widget.
+
+        Parameters
+        ----------
+        brush: QBrush
+            The brush to use when drawing the port widget.
+        """
+        self._line_brush = brush
+
+    def label_visible(self) -> bool:
+        """
+        Return a bool representing the visible state of the ports label.
+
+        Returns
+        -------
+        bool
+        `True` if the label is visible, `False` otherwise.
+        """
+        return self._label_visible
+
+    @Slot(bool)
+    def set_label_visible(self, value: bool) -> None:
+        """
+        Set the visibility of the label.
+
+        Parameter
+        ---------
+        value: bool
+            Sets the label as visible if value is `True`, `False` otherwise.
+        """
+        self._label_visible = value
+
+    def orientation(self) -> Orientation:
+        """
+        Return the orientation of the port widget.
+
+        Returns
+        -------
+        Orientation
+            The current orientation of the port widget.
+        """
+        return self._orientation
+
+    def set_orientation(self, orientation: Orientation) -> None:
+        """
+        Set the orientation of the port widget.
+
+        Parameters
+        ----------
+        orientation: Orientation
+            Sets the orientation of the port widget to one of the alternatives of
+            UP, DOWN, LEFT, RIGHT.
+        """
+        self._orientation = orientation
+
+    def text_font(self) -> QFont:
+        """
+        Return the font of the port widgets label.
+
+        Returns
+        -------
+        QFont
+            The font used for the text in the port widgets label.
+        """
+        return self._text_font
+
+    def set_text_font(self, font: QFont) -> None:
+        """
+        Set the font for the port widgets label.
+
+        Parameters
+        ----------
+        font: QFont
+            Sets the font used in the port widgets label.
+        """
+        self._text_font = font
+
+    line_length = Property(float, line_length, set_line_length)
+    line_width = Property(float, line_width, set_line_width)
+    line_brush = Property(QBrush, line_brush, set_line_brush)
+    label_visible = Property(bool, label_visible, set_label_visible)
+    orientation = Property(Orientation, orientation, set_orientation)
+    text_font = Property(QFont, text_font, set_text_font)
diff --git a/src/simudator/gui/processor_handler.py b/src/simudator/gui/processor_handler.py
index 9a9e9b3321f5fcb82c00be6bd77dbd362751ab53..e0ccb9ac0f5fe249540a5304ea10ad4cb149dff3 100644
--- a/src/simudator/gui/processor_handler.py
+++ b/src/simudator/gui/processor_handler.py
@@ -1,4 +1,3 @@
-from qtpy import QtCore
 from qtpy.QtCore import QObject, QThreadPool
 from qtpy.QtCore import Signal as pyqtSignal
 from qtpy.QtCore import Slot
@@ -21,12 +20,12 @@ class ProcessorHandler(QObject):
 
     running = pyqtSignal(bool)
     """
-    PyQT signal emitted when the processor has started or finished running. 
+    PyQT signal emitted when the processor has started or finished running.
     Emits ``True`` when it has started and ``False`` when it has finished.
     """
     cycle_changed = pyqtSignal(int)
     """
-    PyQT signal emitted when the current clock cycle count of the processor 
+    PyQT signal emitted when the current clock cycle count of the processor
     has changed. Emits the current clock cycle count as an ``int``.
     """
     changed = pyqtSignal()
@@ -105,7 +104,7 @@ class ProcessorHandler(QObject):
     @Slot(int)
     def step_asm_instructions(self, instructions: int):
         """
-        Run some numer of asm instructions.
+        Run some number of asm instructions.
 
         Parameters
         ----------
@@ -333,3 +332,12 @@ class ProcessorHandler(QObject):
         )
         if ok:
             self._update_delay = delay
+
+    @Slot()
+    def handle_module_change(self):
+        """
+        Handle propagating changes when the state of a module has been edited
+        by the user.
+        """
+        pass
+
diff --git a/src/simudator/gui/signal_viewer.py b/src/simudator/gui/signal_viewer.py
index 34aeb20e847fa34689781a710d0ccb6a242fabd1..abd12edbc0be47971e06e19eaba391bceb013d2a 100644
--- a/src/simudator/gui/signal_viewer.py
+++ b/src/simudator/gui/signal_viewer.py
@@ -96,7 +96,7 @@ class SignalViewer(QGraphicsWidget):
         width = self.size().width()
         height = self.size().height()
         margin = self.outline_width / 2
-        return QRectF(0 - margin, 0 - margin, width + margin, height + margin)
+        return QRectF(0 - margin, 0 - margin, width + 2 * margin, height + 2 * margin)
 
     def outline_width(self) -> float:
         """Return the outline pen width.