diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 529fae190..c83c2bcd2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -156,7 +156,8 @@ jobs:
   verify-apps:
     name: Build app
     needs: unit-tests
-    uses: beeware/.github/.github/workflows/app-build-verify.yml@main
+#    uses: beeware/.github/.github/workflows/app-build-verify.yml@main
+    uses: rmartin16/.github-beeware/.github/workflows/app-build-verify.yml@gui-plugin-support
     with:
       # This *must* be the version of Python that is the system Python on the
       # Ubuntu version used to run Linux tests. We use a fixed ubuntu-22.04
@@ -166,6 +167,8 @@ jobs:
       python-version: "3.10"
       runner-os: ${{ matrix.runner-os }}
       framework: ${{ matrix.framework }}
+      briefcase-template-source: https://github.com/rmartin16/briefcase-template.git
+      briefcase-template-branch: gui-plugin-support
     strategy:
       fail-fast: false
       matrix:
diff --git a/changes/1524.feature.rst b/changes/1524.feature.rst
new file mode 100644
index 000000000..cd52cfa51
--- /dev/null
+++ b/changes/1524.feature.rst
@@ -0,0 +1 @@
+Creating new projects with arbitrary third-party GUI frameworks is now supported via plugins.
diff --git a/setup.cfg b/setup.cfg
index 810541366..eedd1a3d2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -112,6 +112,12 @@ where = src
 [options.entry_points]
 console_scripts =
     briefcase = briefcase.__main__:main
+briefcase.wizard.frameworks =
+    Toga = briefcase.plugins.frameworks.toga
+    PySide2 = briefcase.plugins.frameworks.pyside2
+    PySide6 = briefcase.plugins.frameworks.pyside6
+    PursuedPyBear = briefcase.plugins.frameworks.pursuedpybear
+    Pygame = briefcase.plugins.frameworks.pygame
 briefcase.platforms =
     android = briefcase.platforms.android
     iOS = briefcase.platforms.iOS
diff --git a/src/briefcase/commands/new.py b/src/briefcase/commands/new.py
index aa24ce22b..23c2106ba 100644
--- a/src/briefcase/commands/new.py
+++ b/src/briefcase/commands/new.py
@@ -1,7 +1,10 @@
+from __future__ import annotations
+
+import contextlib
 import re
 import unicodedata
 from email.utils import parseaddr
-from typing import Optional
+from types import ModuleType
 from urllib.parse import urlparse
 
 from packaging.version import Version
@@ -17,6 +20,11 @@
 
 from .base import BaseCommand
 
+try:
+    from importlib_metadata import entry_points
+except ImportError:  # pragma: no-cover-if-lt-py310
+    from importlib.metadata import entry_points
+
 
 def titlecase(s):
     """Convert a string to titlecase.
@@ -60,6 +68,14 @@ def titlecase(s):
     )
 
 
+def get_frameworks() -> dict[str, ModuleType]:
+    """Loads built-in and third party GUI frameworks."""
+    return {
+        entry_point.name: entry_point.load()
+        for entry_point in entry_points(group="briefcase.wizard.frameworks")
+    }
+
+
 class NewCommand(BaseCommand):
     cmd_line = "briefcase new"
     command = "new"
@@ -401,21 +417,27 @@ def build_app_context(self):
             ],
         )
 
+        frameworks = get_frameworks()
+        framework_choices = [
+            "Toga",
+            "PySide2       (does not support iOS/Android deployment)",
+            "PySide6       (does not support iOS/Android deployment)",
+            "PursuedPyBear (does not support iOS/Android deployment)",
+            "Pygame        (does not support iOS/Android deployment)",
+        ]
+        builtin_framework_names = [n.split(" ")[0] for n in framework_choices]
+        framework_choices += [
+            f for f in frameworks.keys() if f not in builtin_framework_names
+        ] + ["None"]
+
         gui_framework = self.input_select(
             intro="""
 What GUI toolkit do you want to use for this project?""",
             variable="GUI framework",
-            options=[
-                "Toga",
-                "PySide2 (does not support iOS/Android deployment)",
-                "PySide6 (does not support iOS/Android deployment)",
-                "PursuedPyBear (does not support iOS/Android deployment)",
-                "Pygame (does not support iOS/Android deployment)",
-                "None",
-            ],
+            options=framework_choices,
         )
 
-        return {
+        context = {
             "formal_name": formal_name,
             "app_name": app_name,
             "class_name": class_name,
@@ -427,13 +449,28 @@ def build_app_context(self):
             "bundle": bundle,
             "url": url,
             "license": project_license,
-            "gui_framework": (gui_framework.split())[0],
+        }
+
+        plugin_context = {}
+        if gui_framework != "None":
+            try:
+                plugin = frameworks[gui_framework].plugin(context=context)
+            except KeyError:
+                plugin = frameworks[gui_framework.split(" ")[0]].plugin(context=context)
+            for context_field in plugin.fields:
+                with contextlib.suppress(AttributeError):
+                    if (context_value := getattr(plugin, context_field)()) is not None:
+                        plugin_context[context_field] = context_value
+
+        return {
+            **context,
+            **plugin_context,
         }
 
     def new_app(
         self,
-        template: Optional[str] = None,
-        template_branch: Optional[str] = None,
+        template: str | None = None,
+        template_branch: str | None = None,
         **options,
     ):
         """Ask questions to generate a new application, and generate a stub project from
@@ -520,8 +557,8 @@ def verify_tools(self):
 
     def __call__(
         self,
-        template: Optional[str] = None,
-        template_branch: Optional[str] = None,
+        template: str | None = None,
+        template_branch: str | None = None,
         **options,
     ):
         # Confirm host compatibility, and that all required tools are available.
diff --git a/src/briefcase/plugins/__init__.py b/src/briefcase/plugins/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/briefcase/plugins/frameworks/__init__.py b/src/briefcase/plugins/frameworks/__init__.py
new file mode 100644
index 000000000..bcf0c7eab
--- /dev/null
+++ b/src/briefcase/plugins/frameworks/__init__.py
@@ -0,0 +1,8 @@
+from briefcase.plugins.frameworks.base import BaseGuiPlugin  # noqa: F401
+from briefcase.plugins.frameworks.pursuedpybear import (  # noqa: F401
+    PursuedPyBearGuiPlugin,
+)
+from briefcase.plugins.frameworks.pygame import PygameGuiPlugin  # noqa: F401
+from briefcase.plugins.frameworks.pyside2 import PySide2GuiPlugin  # noqa: F401
+from briefcase.plugins.frameworks.pyside6 import PySide6GuiPlugin  # noqa: F401
+from briefcase.plugins.frameworks.toga import TogaGuiPlugin  # noqa: F401
diff --git a/src/briefcase/plugins/frameworks/base.py b/src/briefcase/plugins/frameworks/base.py
new file mode 100644
index 000000000..f2aa00ee7
--- /dev/null
+++ b/src/briefcase/plugins/frameworks/base.py
@@ -0,0 +1,137 @@
+from __future__ import annotations
+
+from abc import ABC
+from typing import Literal
+
+
+class BaseGuiPlugin(ABC):
+    name: str
+    fields: list[str] = [
+        "app_source",
+        "start_app_source",
+        "requires",
+        "macos_requires",
+        "macos_universal_build",
+        "linux_requires",
+        "linux_system_debian_system_requires",
+        "linux_system_debian_system_runtime_requires",
+        "linux_system_rhel_system_requires",
+        "linux_system_rhel_system_runtime_requires",
+        "linux_system_suse_system_requires",
+        "linux_system_suse_system_runtime_requires",
+        "linux_system_arch_system_requires",
+        "linux_system_arch_system_runtime_requires",
+        "linux_appimage_manylinux",
+        "linux_appimage_system_requires",
+        "linux_appimage_linuxdeploy_plugins",
+        "linux_flatpak_runtime",
+        "linux_flatpak_runtime_version",
+        "linux_flatpak_sdk",
+        "windows_requires",
+        "ios_requires",
+        "ios_supported",
+        "android_requires",
+        "android_supported",
+        "web_requires",
+        "web_supported",
+        "web_style_framework",
+    ]
+
+    def __init__(self, context: dict[str, str | int | bool]):
+        # context contains metadata about the app:
+        #  formal_name
+        #  app_name
+        #  class_name
+        #  module_name
+        #  project_name
+        #  description
+        #  author
+        #  author_email
+        #  bundle
+        #  url
+        #  license
+        self.context = context
+
+    def app_source(self) -> str | None:
+        """The Python source code for the project."""
+
+    def start_app_source(self) -> str | None:
+        """The Python source code to start the app from __main__.py."""
+
+    def requires(self) -> str | None:
+        """List of package requirements for all platforms."""
+
+    def macos_requires(self) -> str | None:
+        """List of package requirements for macOS."""
+
+    def macos_universal_build(self) -> Literal["true", "false"] | None:
+        """Whether to create a universal build for macOS."""
+
+    def linux_requires(self) -> str | None:
+        """List of package requirements for Linux."""
+
+    def linux_system_debian_system_requires(self) -> str | None:
+        """List of system package requirements to build the app."""
+
+    def linux_system_debian_system_runtime_requires(self) -> str | None:
+        """List of system package requirements to run the app on Debian."""
+
+    def linux_system_rhel_system_requires(self) -> str | None:
+        """List of system package requirements to build the app on RHEL."""
+
+    def linux_system_rhel_system_runtime_requires(self) -> str | None:
+        """List of system package requirements to run the app on RHEL."""
+
+    def linux_system_suse_system_requires(self) -> str | None:
+        """List of system package requirements to build the app on SUSE."""
+
+    def linux_system_suse_system_runtime_requires(self) -> str | None:
+        """List of system package requirements to run the app on SUSE."""
+
+    def linux_system_arch_system_requires(self) -> str | None:
+        """List of system package requirements to build the app on Arch."""
+
+    def linux_system_arch_system_runtime_requires(self) -> str | None:
+        """List of system package requirements to run the app on Arch."""
+
+    def linux_appimage_manylinux(self) -> str | None:
+        """The manylinux base, e.g. manylinux2014, to use to build the app."""
+
+    def linux_appimage_system_requires(self) -> str | None:
+        """List of system package requirements to build the app in to an AppImage."""
+
+    def linux_appimage_linuxdeploy_plugins(self) -> str | None:
+        """List of linuxdeploy plugins to use to build the app in to an AppImage."""
+
+    def linux_flatpak_runtime(self) -> str | None:
+        """The Flatpak runtime, e.g. org.gnome.Platform, for the app."""
+
+    def linux_flatpak_runtime_version(self) -> str | None:
+        """The Flatpak runtime version, e.g. 44, for the app."""
+
+    def linux_flatpak_sdk(self) -> str | None:
+        """The Flatpak SDK, e.g. org.gnome.Sdk, for the app."""
+
+    def windows_requires(self) -> str | None:
+        """List of package requirements for Windows."""
+
+    def ios_requires(self) -> str | None:
+        """List of package requirements for iOS."""
+
+    def ios_supported(self) -> Literal["true", "false"] | None:
+        """Whether the GUI framework supports iOS."""
+
+    def android_requires(self) -> str | None:
+        """List of package requirements for Android."""
+
+    def android_supported(self) -> Literal["true", "false"] | None:
+        """Whether the GUI framework supports Android."""
+
+    def web_requires(self) -> str | None:
+        """List of package requirements for Web."""
+
+    def web_supported(self) -> Literal["true", "false"] | None:
+        """Whether the GUI framework supports Web."""
+
+    def web_style_framework(self) -> str | None:
+        """The style framework, e.g. Bootstrap or Shoelace, for web."""
diff --git a/src/briefcase/plugins/frameworks/pursuedpybear.py b/src/briefcase/plugins/frameworks/pursuedpybear.py
new file mode 100644
index 000000000..cc852b50c
--- /dev/null
+++ b/src/briefcase/plugins/frameworks/pursuedpybear.py
@@ -0,0 +1,71 @@
+from briefcase.plugins.frameworks.base import BaseGuiPlugin
+
+
+class PursuedPyBearGuiPlugin(BaseGuiPlugin):
+    name = "PursuedPyBear"
+
+    def app_source(self):
+        return """
+import os
+import sys
+
+try:
+    from importlib import metadata as importlib_metadata
+except ImportError:
+    # Backwards compatibility - importlib.metadata was added in Python 3.8
+    import importlib_metadata
+
+import ppb
+
+
+class {{ cookiecutter.class_name }}(ppb.Scene):
+    def __init__(self, **props):
+        super().__init__(**props)
+
+        self.add(ppb.Sprite(
+            image=ppb.Image('{{ cookiecutter.module_name }}/resources/{{ cookiecutter.app_name }}.png'),
+        ))
+
+
+def main():
+    # Linux desktop environments use app's .desktop file to integrate the app
+    # to their application menus. The .desktop file of this app will include
+    # StartupWMClass key, set to app's formal name, which helps associate
+    # app's windows to its menu item.
+    #
+    # For association to work any windows of the app must have WMCLASS
+    # property set to match the value set in app's desktop file. For PPB this
+    # is set using environment variable.
+
+    # Find the name of the module that was used to start the app
+    app_module = sys.modules['__main__'].__package__
+    # Retrieve the app's metadata
+    metadata = importlib_metadata.metadata(app_module)
+
+    os.environ['SDL_VIDEO_X11_WMCLASS'] = metadata['Formal-Name']
+
+    ppb.run(
+        starting_scene={{ cookiecutter.class_name }},
+        title=metadata['Formal-Name'],
+    )
+        """
+
+    def requires(self):
+        return """
+    "ppb~=1.1",
+"""
+
+    def linux_appimage_manylinux(self):
+        return "manylinux2014"
+
+    def linux_flatpak_runtime(self):
+        return "org.freedesktop.Platform"
+
+    def linux_flatpak_runtime_version(self):
+        return "22.08"
+
+    def linux_flatpak_sdk(self):
+        return "org.freedesktop.Sdk"
+
+
+plugin = PursuedPyBearGuiPlugin
diff --git a/src/briefcase/plugins/frameworks/pygame.py b/src/briefcase/plugins/frameworks/pygame.py
new file mode 100644
index 000000000..e9ebfe7b8
--- /dev/null
+++ b/src/briefcase/plugins/frameworks/pygame.py
@@ -0,0 +1,76 @@
+from briefcase.plugins.frameworks.base import BaseGuiPlugin
+
+
+class PygameGuiPlugin(BaseGuiPlugin):
+    name = "Pygame"
+
+    def app_source(self):
+        return """
+import os
+import sys
+
+import pygame
+
+try:
+    from importlib import metadata as importlib_metadata
+except ImportError:
+    # Backwards compatibility - importlib.metadata was added in Python 3.8
+    import importlib_metadata
+
+SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
+WHITE = (255, 255, 255)
+
+
+def main():
+    # Linux desktop environments use app's .desktop file to integrate the app
+    # to their application menus. The .desktop file of this app will include
+    # StartupWMClass key, set to app's formal name, which helps associate
+    # app's windows to its menu item.
+    #
+    # For association to work any windows of the app must have WMCLASS
+    # property set to match the value set in app's desktop file. For PPB this
+    # is set using environment variable.
+
+    # Find the name of the module that was used to start the app
+    app_module = sys.modules["__main__"].__package__
+    # Retrieve the app's metadata
+    metadata = importlib_metadata.metadata(app_module)
+
+    os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"]
+
+    pygame.init()
+    pygame.display.set_caption(metadata["Formal-Name"])
+    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
+
+    running = True
+    while running:
+        for event in pygame.event.get():
+            if event.type == pygame.QUIT:
+                running = False
+                break
+
+        screen.fill(WHITE)
+        pygame.display.flip()
+
+    pygame.quit()
+        """
+
+    def requires(self):
+        return """
+    "pygame~=2.2",
+"""
+
+    def linux_appimage_manylinux(self):
+        return "manylinux2014"
+
+    def linux_flatpak_runtime(self):
+        return "org.freedesktop.Platform"
+
+    def linux_flatpak_runtime_version(self):
+        return "22.08"
+
+    def linux_flatpak_sdk(self):
+        return "org.freedesktop.Sdk"
+
+
+plugin = PygameGuiPlugin
diff --git a/src/briefcase/plugins/frameworks/pyside2.py b/src/briefcase/plugins/frameworks/pyside2.py
new file mode 100644
index 000000000..f2e155a1c
--- /dev/null
+++ b/src/briefcase/plugins/frameworks/pyside2.py
@@ -0,0 +1,133 @@
+from briefcase.plugins.frameworks.base import BaseGuiPlugin
+
+
+class PySide2GuiPlugin(BaseGuiPlugin):
+    name = "PySide2"
+
+    def app_source(self):
+        return """
+import sys
+
+try:
+    from importlib import metadata as importlib_metadata
+except ImportError:
+    # Backwards compatibility - importlib.metadata was added in Python 3.8
+    import importlib_metadata
+
+from PySide2 import QtWidgets
+
+
+class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow):
+    def __init__(self):
+        super().__init__()
+        self.init_ui()
+
+    def init_ui(self):
+        self.setWindowTitle('{{ cookiecutter.app_name }}')
+        self.show()
+
+
+def main():
+    # Linux desktop environments use app's .desktop file to integrate the app
+    # to their application menus. The .desktop file of this app will include
+    # StartupWMClass key, set to app's formal name, which helps associate
+    # app's windows to its menu item.
+    #
+    # For association to work any windows of the app must have WMCLASS
+    # property set to match the value set in app's desktop file. For PySide2
+    # this is set with setApplicationName().
+
+    # Find the name of the module that was used to start the app
+    app_module = sys.modules['__main__'].__package__
+    # Retrieve the app's metadata
+    metadata = importlib_metadata.metadata(app_module)
+
+    QtWidgets.QApplication.setApplicationName(metadata['Formal-Name'])
+
+    app = QtWidgets.QApplication(sys.argv)
+    main_window = {{ cookiecutter.class_name }}()
+    sys.exit(app.exec_())
+"""
+
+    def requires(self):
+        return """
+    "pyside2~=5.15",
+"""
+
+    def macos_requires(self):
+        return """
+    "toga-cocoa~=0.4.0",
+"""
+
+    def linux_system_debian_system_requires(self):
+        return ""
+
+    def linux_system_debian_system_runtime_requires(self):
+        return """
+    # Derived from https://doc.qt.io/qt-6/linux-requirements.html
+    "libxrender1",
+    "libxcb-render0",
+    "libxcb-render-util0",
+    "libxcb-shape0",
+    "libxcb-randr0",
+    "libxcb-xfixes0",
+    "libxcb-xkb1",
+    "libxcb-sync1",
+    "libxcb-shm0",
+    "libxcb-icccm4",
+    "libxcb-keysyms1",
+    "libxcb-image0",
+    "libxcb-util1",
+    "libxkbcommon0",
+    "libxkbcommon-x11-0",
+    "libfontconfig1",
+    "libfreetype6",
+    "libxext6",
+    "libx11-6",
+    "libxcb1",
+    "libx11-xcb1",
+    "libsm6",
+    "libice6",
+    "libglib2.0-0",
+    "libgl1",
+    "libegl1-mesa",
+    "libdbus-1-3",
+    "libgssapi-krb5-2",
+"""
+
+    def linux_system_rhel_system_requires(self):
+        return ""
+
+    def linux_system_rhel_system_runtime_requires(self):
+        return """
+    "qt5-qtbase-gui",
+"""
+
+    def linux_system_suse_system_requires(self):
+        return ""
+
+    def linux_system_suse_system_runtime_requires(self):
+        return """
+    "libQt5Gui5",
+"""
+
+    def linux_system_arch_system_requires(self):
+        return ""
+
+    def linux_system_arch_system_runtime_requires(self):
+        return ""
+
+    def linux_appimage_manylinux(self):
+        return "manylinux2014"
+
+    def linux_flatpak_runtime(self):
+        return "org.kde.Platform"
+
+    def linux_flatpak_runtime_version(self):
+        return "6.4"
+
+    def linux_flatpak_sdk(self):
+        return "org.kde.Sdk"
+
+
+plugin = PySide2GuiPlugin
diff --git a/src/briefcase/plugins/frameworks/pyside6.py b/src/briefcase/plugins/frameworks/pyside6.py
new file mode 100644
index 000000000..40cbb0822
--- /dev/null
+++ b/src/briefcase/plugins/frameworks/pyside6.py
@@ -0,0 +1,133 @@
+from briefcase.plugins.frameworks.base import BaseGuiPlugin
+
+
+class PySide6GuiPlugin(BaseGuiPlugin):
+    name = "PySide6"
+
+    def app_source(self):
+        return """
+import sys
+
+try:
+    from importlib import metadata as importlib_metadata
+except ImportError:
+    # Backwards compatibility - importlib.metadata was added in Python 3.8
+    import importlib_metadata
+
+from PySide6 import QtWidgets
+
+
+class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow):
+    def __init__(self):
+        super().__init__()
+        self.init_ui()
+
+    def init_ui(self):
+        self.setWindowTitle('{{ cookiecutter.app_name }}')
+        self.show()
+
+
+def main():
+    # Linux desktop environments use app's .desktop file to integrate the app
+    # to their application menus. The .desktop file of this app will include
+    # StartupWMClass key, set to app's formal name, which helps associate
+    # app's windows to its menu item.
+    #
+    # For association to work any windows of the app must have WMCLASS
+    # property set to match the value set in app's desktop file. For PySide2
+    # this is set with setApplicationName().
+
+    # Find the name of the module that was used to start the app
+    app_module = sys.modules['__main__'].__package__
+    # Retrieve the app's metadata
+    metadata = importlib_metadata.metadata(app_module)
+
+    QtWidgets.QApplication.setApplicationName(metadata['Formal-Name'])
+
+    app = QtWidgets.QApplication(sys.argv)
+    main_window = {{ cookiecutter.class_name }}()
+    sys.exit(app.exec())
+"""
+
+    def requires(self):
+        return """
+    "PySide6-Essentials~=6.5",
+    # "PySide6-Addons~=6.5",
+"""
+
+    def linux_system_debian_system_requires(self):
+        return ""
+
+    def linux_system_debian_system_runtime_requires(self):
+        return """
+    # Derived from https://doc.qt.io/qt-6/linux-requirements.html
+    "libxrender1",
+    "libxcb-render0",
+    "libxcb-render-util0",
+    "libxcb-shape0",
+    "libxcb-randr0",
+    "libxcb-xfixes0",
+    "libxcb-xkb1",
+    "libxcb-sync1",
+    "libxcb-shm0",
+    "libxcb-icccm4",
+    "libxcb-keysyms1",
+    "libxcb-image0",
+    "libxcb-util1",
+    "libxkbcommon0",
+    "libxkbcommon-x11-0",
+    "libfontconfig1",
+    "libfreetype6",
+    "libxext6",
+    "libx11-6",
+    "libxcb1",
+    "libx11-xcb1",
+    "libsm6",
+    "libice6",
+    "libglib2.0-0",
+    "libgl1",
+    "libegl1-mesa",
+    "libdbus-1-3",
+    "libgssapi-krb5-2",
+"""
+
+    def linux_system_rhel_system_requires(self):
+        return ""
+
+    def linux_system_rhel_system_runtime_requires(self):
+        return """
+    "qt6-qtbase-gui",
+"""
+
+    def linux_system_suse_system_requires(self):
+        return ""
+
+    def linux_system_suse_system_runtime_requires(self):
+        return """
+    "libQt6Gui6",
+"""
+
+    def linux_system_arch_system_requires(self):
+        return """
+    "qt6-base",
+"""
+
+    def linux_system_arch_system_runtime_requires(self):
+        return """
+    "qt6-base",
+"""
+
+    def linux_appimage_manylinux(self):
+        return "manylinux_2_28"
+
+    def linux_flatpak_runtime(self):
+        return "org.kde.Platform"
+
+    def linux_flatpak_runtime_version(self):
+        return "6.4"
+
+    def linux_flatpak_sdk(self):
+        return "org.kde.Sdk"
+
+
+plugin = PySide6GuiPlugin
diff --git a/src/briefcase/plugins/frameworks/toga.py b/src/briefcase/plugins/frameworks/toga.py
new file mode 100644
index 000000000..16ec6fdd0
--- /dev/null
+++ b/src/briefcase/plugins/frameworks/toga.py
@@ -0,0 +1,207 @@
+from briefcase.plugins.frameworks.base import BaseGuiPlugin
+
+
+class TogaGuiPlugin(BaseGuiPlugin):
+    name = "Toga"
+
+    def app_source(self):
+        return '''
+import toga
+from toga.style import Pack
+from toga.style.pack import COLUMN, ROW
+
+
+class {{ cookiecutter.class_name }}(toga.App):
+
+    def startup(self):
+        """Construct and show the Toga application.
+
+        Usually, you would add your application to a main content box. We then create a
+        main window (with a name matching the app), and show the main window.
+        """
+        main_box = toga.Box()
+
+        self.main_window = toga.MainWindow(title=self.formal_name)
+        self.main_window.content = main_box
+        self.main_window.show()
+
+
+def main():
+    return {{ cookiecutter.class_name }}()
+'''
+
+    def start_app_source(self):
+        return "    main().main_loop()"
+
+    def requires(self):
+        return None
+
+    def macos_requires(self):
+        return """
+    "toga-cocoa~=0.4.0",
+    "std-nslog~=1.0.0",
+"""
+
+    def macos_universal_build(self):
+        return "true"
+
+    def linux_requires(self):
+        return """
+    "toga-gtk~=0.4.0",
+"""
+
+    def linux_system_debian_system_requires(self):
+        return """
+    # Needed to compile pycairo wheel
+    "libcairo2-dev",
+    # Needed to compile PyGObject wheel
+    "libgirepository1.0-dev",
+"""
+
+    def linux_system_debian_system_runtime_requires(self):
+        return """
+    # Needed to provide GTK and its GI bindings
+    "gir1.2-gtk-3.0",
+    "libgirepository-1.0-1",
+    # Dependencies that GTK looks for at runtime
+    "libcanberra-gtk3-module",
+    # Needed to provide WebKit2 at runtime
+    # "gir1.2-webkit2-4.0",
+"""
+
+    def linux_system_rhel_system_requires(self):
+        return """
+    # Needed to compile pycairo wheel
+    "cairo-gobject-devel",
+    # Needed to compile PyGObject wheel
+    "gobject-introspection-devel",
+"""
+
+    def linux_system_rhel_system_runtime_requires(self):
+        return """
+    # Needed to support Python bindings to GTK
+    "gobject-introspection",
+    # Needed to provide GTK
+    "gtk3",
+    # Dependencies that GTK looks for at runtime
+    "libcanberra-gtk3",
+    # Needed to provide WebKit2 at runtime
+    # "webkit2gtk3",
+"""
+
+    def linux_system_suse_system_requires(self):
+        return """
+    # Needed to compile pycairo wheel
+    "cairo-devel",
+    # Needed to compile PyGObject wheel
+    "gobject-introspection-devel",
+"""
+
+    def linux_system_suse_system_runtime_requires(self):
+        return """
+    # Needed to provide GTK
+    "gtk3",
+    # Needed to support Python bindings to GTK
+    "gobject-introspection", "typelib(Gtk)=3.0",
+    # Dependencies that GTK looks for at runtime
+    "libcanberra-gtk3-0",
+    # Needed to provide WebKit2 at runtime
+    # "libwebkit2gtk3",
+    # "typelib(WebKit2)",
+"""
+
+    def linux_system_arch_system_requires(self):
+        return """
+    # Needed to compile pycairo wheel
+    "cairo",
+    # Needed to compile PyGObject wheel
+    "gobject-introspection",
+    # Runtime dependencies that need to exist so that the
+    # Arch package passes final validation.
+    # Needed to provide GTK
+    "gtk3",
+    # Dependencies that GTK looks for at runtime
+    "libcanberra",
+    # Needed to provide WebKit2
+    # "webkit2gtk",
+"""
+
+    def linux_system_arch_system_runtime_requires(self):
+        return """
+    # Needed to provide GTK
+    "gtk3",
+    # Needed to provide PyGObject bindings
+    "gobject-introspection-runtime",
+    # Dependencies that GTK looks for at runtime
+    "libcanberra",
+    # Needed to provide WebKit2 at runtime
+    # "webkit2gtk",
+"""
+
+    def linux_appimage_manylinux(self):
+        return "manylinux2014"
+
+    def linux_appimage_system_requires(self):
+        return """
+    # Needed to compile pycairo wheel
+    "cairo-gobject-devel",
+    # Needed to compile PyGObject wheel
+    "gobject-introspection-devel",
+    # Needed to provide GTK
+    "gtk3-devel",
+    # Dependencies that GTK looks for at runtime, that need to be
+    # in the build environment to be picked up by linuxdeploy
+    "libcanberra-gtk3",
+    "PackageKit-gtk3-module",
+    "gvfs-client",
+"""
+
+    def linux_appimage_linuxdeploy_plugins(self):
+        return """
+    "DEPLOY_GTK_VERSION=3 gtk",
+"""
+
+    def linux_flatpak_runtime(self):
+        return "org.gnome.Platform"
+
+    def linux_flatpak_runtime_version(self):
+        return "44"
+
+    def linux_flatpak_sdk(self):
+        return "org.gnome.Sdk"
+
+    def windows_requires(self):
+        return """
+    "toga-winforms~=0.4.0",
+"""
+
+    def ios_requires(self):
+        return """
+    "toga-iOS~=0.4.0",
+    "std-nslog~=1.0.0",
+"""
+
+    def ios_supported(self):
+        return "true"
+
+    def android_requires(self):
+        return """
+    "toga-android~=0.4.0",
+"""
+
+    def android_supported(self):
+        return "true"
+
+    def web_requires(self):
+        return """
+    "toga-web~=0.4.0",
+"""
+
+    def web_supported(self):
+        return "true"
+
+    def web_style_framework(self):
+        return "Shoelace v2.3"
+
+
+plugin = TogaGuiPlugin
diff --git a/tests/commands/new/test_build_app_context.py b/tests/commands/new/test_build_app_context.py
index 238983738..08f0e58ac 100644
--- a/tests/commands/new/test_build_app_context.py
+++ b/tests/commands/new/test_build_app_context.py
@@ -1,4 +1,4 @@
-def test_question_sequence(new_command):
+def test_question_sequence_toga(new_command):
     """Questions are asked, a context is constructed."""
 
     # Prime answers for all the questions.
@@ -12,28 +12,296 @@ def test_question_sequence(new_command):
         "grace@navy.mil",  # author email
         "https://navy.mil/myapplication",  # URL
         "4",  # license
-        "1",  # GUI toolkit
+        "1",  # Toga GUI toolkit
     ]
 
     assert new_command.build_app_context() == {
-        "formal_name": "My Application",
-        "class_name": "MyApplication",
+        "android_requires": '\n    "toga-android~=0.4.0",\n',
+        "android_supported": "true",
         "app_name": "myapplication",
-        "module_name": "myapplication",
+        "app_source": "\n"
+        "import toga\n"
+        "from toga.style import Pack\n"
+        "from toga.style.pack import COLUMN, ROW\n"
+        "\n"
+        "\n"
+        "class {{ cookiecutter.class_name }}(toga.App):\n"
+        "\n"
+        "    def startup(self):\n"
+        '        """Construct and show the Toga application.\n'
+        "\n"
+        "        Usually, you would add your application to a main "
+        "content box. We then create a\n"
+        "        main window (with a name matching the app), and show "
+        "the main window.\n"
+        '        """\n'
+        "        main_box = toga.Box()\n"
+        "\n"
+        "        self.main_window = "
+        "toga.MainWindow(title=self.formal_name)\n"
+        "        self.main_window.content = main_box\n"
+        "        self.main_window.show()\n"
+        "\n"
+        "\n"
+        "def main():\n"
+        "    return {{ cookiecutter.class_name }}()\n",
+        "author": "Grace Hopper",
+        "author_email": "grace@navy.mil",
         "bundle": "org.beeware",
-        "project_name": "My Project",
+        "class_name": "MyApplication",
         "description": "Cool stuff",
+        "formal_name": "My Application",
+        "ios_requires": '\n    "toga-iOS~=0.4.0",\n    "std-nslog~=1.0.0",\n',
+        "ios_supported": "true",
+        "license": "GNU General Public License v2 (GPLv2)",
+        "linux_appimage_linuxdeploy_plugins": '\n    "DEPLOY_GTK_VERSION=3 gtk",\n',
+        "linux_appimage_manylinux": "manylinux2014",
+        "linux_appimage_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo-gobject-devel",\n'
+        "    # Needed to compile PyGObject wheel\n"
+        '    "gobject-introspection-devel",\n'
+        "    # Needed to provide GTK\n"
+        '    "gtk3-devel",\n'
+        "    # Dependencies that GTK looks for at "
+        "runtime, that need to be\n"
+        "    # in the build environment to be "
+        "picked up by linuxdeploy\n"
+        '    "libcanberra-gtk3",\n'
+        '    "PackageKit-gtk3-module",\n'
+        '    "gvfs-client",\n',
+        "linux_flatpak_runtime": "org.gnome.Platform",
+        "linux_flatpak_runtime_version": "44",
+        "linux_flatpak_sdk": "org.gnome.Sdk",
+        "linux_requires": '\n    "toga-gtk~=0.4.0",\n',
+        "linux_system_arch_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "gobject-introspection",\n'
+        "    # Runtime dependencies that need to "
+        "exist so that the\n"
+        "    # Arch package passes final "
+        "validation.\n"
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Dependencies that GTK looks for "
+        "at runtime\n"
+        '    "libcanberra",\n'
+        "    # Needed to provide WebKit2\n"
+        '    # "webkit2gtk",\n',
+        "linux_system_arch_system_runtime_requires": "\n"
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Needed to provide "
+        "PyGObject bindings\n"
+        "    "
+        '"gobject-introspection-runtime",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        '    "libcanberra",\n'
+        "    # Needed to provide WebKit2 "
+        "at runtime\n"
+        '    # "webkit2gtk",\n',
+        "linux_system_debian_system_requires": "\n"
+        "    # Needed to compile pycairo "
+        "wheel\n"
+        '    "libcairo2-dev",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "libgirepository1.0-dev",\n',
+        "linux_system_debian_system_runtime_requires": "\n"
+        "    # Needed to provide GTK "
+        "and its GI bindings\n"
+        '    "gir1.2-gtk-3.0",\n'
+        '    "libgirepository-1.0-1",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        "    "
+        '"libcanberra-gtk3-module",\n'
+        "    # Needed to provide "
+        "WebKit2 at runtime\n"
+        '    # "gir1.2-webkit2-4.0",\n',
+        "linux_system_rhel_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo-gobject-devel",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "gobject-introspection-devel",\n',
+        "linux_system_rhel_system_runtime_requires": "\n"
+        "    # Needed to support Python "
+        "bindings to GTK\n"
+        '    "gobject-introspection",\n'
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        '    "libcanberra-gtk3",\n'
+        "    # Needed to provide WebKit2 "
+        "at runtime\n"
+        '    # "webkit2gtk3",\n',
+        "linux_system_suse_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo-devel",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "gobject-introspection-devel",\n',
+        "linux_system_suse_system_runtime_requires": "\n"
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Needed to support Python "
+        "bindings to GTK\n"
+        '    "gobject-introspection", '
+        '"typelib(Gtk)=3.0",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        '    "libcanberra-gtk3-0",\n'
+        "    # Needed to provide WebKit2 "
+        "at runtime\n"
+        '    # "libwebkit2gtk3",\n'
+        '    # "typelib(WebKit2)",\n',
+        "macos_requires": '\n    "toga-cocoa~=0.4.0",\n    "std-nslog~=1.0.0",\n',
+        "macos_universal_build": "true",
+        "module_name": "myapplication",
+        "project_name": "My Project",
+        "start_app_source": "    main().main_loop()",
+        "url": "https://navy.mil/myapplication",
+        "web_requires": '\n    "toga-web~=0.4.0",\n',
+        "web_style_framework": "Shoelace v2.3",
+        "web_supported": "true",
+        "windows_requires": '\n    "toga-winforms~=0.4.0",\n',
+    }
+
+
+def test_question_sequence_pyside2(new_command):
+    """Questions are asked, a context is constructed."""
+
+    # Prime answers for all the questions.
+    new_command.input.values = [
+        "My Application",  # formal name
+        "",  # app name - accept the default
+        "org.beeware",  # bundle ID
+        "My Project",  # project name
+        "Cool stuff",  # description
+        "Grace Hopper",  # author
+        "grace@navy.mil",  # author email
+        "https://navy.mil/myapplication",  # URL
+        "4",  # license
+        "2",  # PySide2 GUI toolkit
+    ]
+
+    assert new_command.build_app_context() == {
+        "app_name": "myapplication",
+        "app_source": "\n"
+        "import sys\n"
+        "\n"
+        "try:\n"
+        "    from importlib import metadata as importlib_metadata\n"
+        "except ImportError:\n"
+        "    # Backwards compatibility - importlib.metadata was added "
+        "in Python 3.8\n"
+        "    import importlib_metadata\n"
+        "\n"
+        "from PySide2 import QtWidgets\n"
+        "\n"
+        "\n"
+        "class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow):\n"
+        "    def __init__(self):\n"
+        "        super().__init__()\n"
+        "        self.init_ui()\n"
+        "\n"
+        "    def init_ui(self):\n"
+        "        self.setWindowTitle('{{ cookiecutter.app_name }}')\n"
+        "        self.show()\n"
+        "\n"
+        "\n"
+        "def main():\n"
+        "    # Linux desktop environments use app's .desktop file to "
+        "integrate the app\n"
+        "    # to their application menus. The .desktop file of this "
+        "app will include\n"
+        "    # StartupWMClass key, set to app's formal name, which "
+        "helps associate\n"
+        "    # app's windows to its menu item.\n"
+        "    #\n"
+        "    # For association to work any windows of the app must have "
+        "WMCLASS\n"
+        "    # property set to match the value set in app's desktop "
+        "file. For PySide2\n"
+        "    # this is set with setApplicationName().\n"
+        "\n"
+        "    # Find the name of the module that was used to start the "
+        "app\n"
+        "    app_module = sys.modules['__main__'].__package__\n"
+        "    # Retrieve the app's metadata\n"
+        "    metadata = importlib_metadata.metadata(app_module)\n"
+        "\n"
+        "    "
+        "QtWidgets.QApplication.setApplicationName(metadata['Formal-Name'])\n"
+        "\n"
+        "    app = QtWidgets.QApplication(sys.argv)\n"
+        "    main_window = {{ cookiecutter.class_name }}()\n"
+        "    sys.exit(app.exec_())\n",
         "author": "Grace Hopper",
         "author_email": "grace@navy.mil",
-        "url": "https://navy.mil/myapplication",
+        "bundle": "org.beeware",
+        "class_name": "MyApplication",
+        "description": "Cool stuff",
+        "formal_name": "My Application",
         "license": "GNU General Public License v2 (GPLv2)",
-        "gui_framework": "Toga",
+        "linux_appimage_manylinux": "manylinux2014",
+        "linux_flatpak_runtime": "org.kde.Platform",
+        "linux_flatpak_runtime_version": "6.4",
+        "linux_flatpak_sdk": "org.kde.Sdk",
+        "linux_system_arch_system_requires": "",
+        "linux_system_arch_system_runtime_requires": "",
+        "linux_system_debian_system_requires": "",
+        "linux_system_debian_system_runtime_requires": "\n"
+        "    # Derived from "
+        "https://doc.qt.io/qt-6/linux-requirements.html\n"
+        '    "libxrender1",\n'
+        '    "libxcb-render0",\n'
+        '    "libxcb-render-util0",\n'
+        '    "libxcb-shape0",\n'
+        '    "libxcb-randr0",\n'
+        '    "libxcb-xfixes0",\n'
+        '    "libxcb-xkb1",\n'
+        '    "libxcb-sync1",\n'
+        '    "libxcb-shm0",\n'
+        '    "libxcb-icccm4",\n'
+        '    "libxcb-keysyms1",\n'
+        '    "libxcb-image0",\n'
+        '    "libxcb-util1",\n'
+        '    "libxkbcommon0",\n'
+        '    "libxkbcommon-x11-0",\n'
+        '    "libfontconfig1",\n'
+        '    "libfreetype6",\n'
+        '    "libxext6",\n'
+        '    "libx11-6",\n'
+        '    "libxcb1",\n'
+        '    "libx11-xcb1",\n'
+        '    "libsm6",\n'
+        '    "libice6",\n'
+        '    "libglib2.0-0",\n'
+        '    "libgl1",\n'
+        '    "libegl1-mesa",\n'
+        '    "libdbus-1-3",\n'
+        '    "libgssapi-krb5-2",\n',
+        "linux_system_rhel_system_requires": "",
+        "linux_system_rhel_system_runtime_requires": '\n    "qt5-qtbase-gui",\n',
+        "linux_system_suse_system_requires": "",
+        "linux_system_suse_system_runtime_requires": '\n    "libQt5Gui5",\n',
+        "macos_requires": '\n    "toga-cocoa~=0.4.0",\n',
+        "module_name": "myapplication",
+        "project_name": "My Project",
+        "requires": '\n    "pyside2~=5.15",\n',
+        "url": "https://navy.mil/myapplication",
     }
 
 
-def test_question_sequence_with_nondefault_gui(new_command):
-    """Questions are asked, a context is constructed, but the GUI option is formatted to
-    extract the GUI name."""
+def test_question_sequence_pyside6(new_command):
+    """Questions are asked, a context is constructed."""
 
     # Prime answers for all the questions.
     new_command.input.values = [
@@ -46,22 +314,331 @@ def test_question_sequence_with_nondefault_gui(new_command):
         "grace@navy.mil",  # author email
         "https://navy.mil/myapplication",  # URL
         "4",  # license
-        "2",  # GUI toolkit
+        "3",  # PySide6 GUI toolkit
     ]
 
     assert new_command.build_app_context() == {
-        "formal_name": "My Application",
-        "class_name": "MyApplication",
         "app_name": "myapplication",
-        "module_name": "myapplication",
+        "app_source": "\n"
+        "import sys\n"
+        "\n"
+        "try:\n"
+        "    from importlib import metadata as importlib_metadata\n"
+        "except ImportError:\n"
+        "    # Backwards compatibility - importlib.metadata was added "
+        "in Python 3.8\n"
+        "    import importlib_metadata\n"
+        "\n"
+        "from PySide6 import QtWidgets\n"
+        "\n"
+        "\n"
+        "class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow):\n"
+        "    def __init__(self):\n"
+        "        super().__init__()\n"
+        "        self.init_ui()\n"
+        "\n"
+        "    def init_ui(self):\n"
+        "        self.setWindowTitle('{{ cookiecutter.app_name }}')\n"
+        "        self.show()\n"
+        "\n"
+        "\n"
+        "def main():\n"
+        "    # Linux desktop environments use app's .desktop file to "
+        "integrate the app\n"
+        "    # to their application menus. The .desktop file of this "
+        "app will include\n"
+        "    # StartupWMClass key, set to app's formal name, which "
+        "helps associate\n"
+        "    # app's windows to its menu item.\n"
+        "    #\n"
+        "    # For association to work any windows of the app must have "
+        "WMCLASS\n"
+        "    # property set to match the value set in app's desktop "
+        "file. For PySide2\n"
+        "    # this is set with setApplicationName().\n"
+        "\n"
+        "    # Find the name of the module that was used to start the "
+        "app\n"
+        "    app_module = sys.modules['__main__'].__package__\n"
+        "    # Retrieve the app's metadata\n"
+        "    metadata = importlib_metadata.metadata(app_module)\n"
+        "\n"
+        "    "
+        "QtWidgets.QApplication.setApplicationName(metadata['Formal-Name'])\n"
+        "\n"
+        "    app = QtWidgets.QApplication(sys.argv)\n"
+        "    main_window = {{ cookiecutter.class_name }}()\n"
+        "    sys.exit(app.exec())\n",
+        "author": "Grace Hopper",
+        "author_email": "grace@navy.mil",
         "bundle": "org.beeware",
+        "class_name": "MyApplication",
+        "description": "Cool stuff",
+        "formal_name": "My Application",
+        "license": "GNU General Public License v2 (GPLv2)",
+        "linux_appimage_manylinux": "manylinux_2_28",
+        "linux_flatpak_runtime": "org.kde.Platform",
+        "linux_flatpak_runtime_version": "6.4",
+        "linux_flatpak_sdk": "org.kde.Sdk",
+        "linux_system_arch_system_requires": '\n    "qt6-base",\n',
+        "linux_system_arch_system_runtime_requires": '\n    "qt6-base",\n',
+        "linux_system_debian_system_requires": "",
+        "linux_system_debian_system_runtime_requires": "\n"
+        "    # Derived from "
+        "https://doc.qt.io/qt-6/linux-requirements.html\n"
+        '    "libxrender1",\n'
+        '    "libxcb-render0",\n'
+        '    "libxcb-render-util0",\n'
+        '    "libxcb-shape0",\n'
+        '    "libxcb-randr0",\n'
+        '    "libxcb-xfixes0",\n'
+        '    "libxcb-xkb1",\n'
+        '    "libxcb-sync1",\n'
+        '    "libxcb-shm0",\n'
+        '    "libxcb-icccm4",\n'
+        '    "libxcb-keysyms1",\n'
+        '    "libxcb-image0",\n'
+        '    "libxcb-util1",\n'
+        '    "libxkbcommon0",\n'
+        '    "libxkbcommon-x11-0",\n'
+        '    "libfontconfig1",\n'
+        '    "libfreetype6",\n'
+        '    "libxext6",\n'
+        '    "libx11-6",\n'
+        '    "libxcb1",\n'
+        '    "libx11-xcb1",\n'
+        '    "libsm6",\n'
+        '    "libice6",\n'
+        '    "libglib2.0-0",\n'
+        '    "libgl1",\n'
+        '    "libegl1-mesa",\n'
+        '    "libdbus-1-3",\n'
+        '    "libgssapi-krb5-2",\n',
+        "linux_system_rhel_system_requires": "",
+        "linux_system_rhel_system_runtime_requires": '\n    "qt6-qtbase-gui",\n',
+        "linux_system_suse_system_requires": "",
+        "linux_system_suse_system_runtime_requires": '\n    "libQt6Gui6",\n',
+        "module_name": "myapplication",
         "project_name": "My Project",
+        "requires": '\n    "PySide6-Essentials~=6.5",\n    # "PySide6-Addons~=6.5",\n',
+        "url": "https://navy.mil/myapplication",
+    }
+
+
+def test_question_sequence_pursuedpybear(new_command):
+    """Questions are asked, a context is constructed."""
+
+    # Prime answers for all the questions.
+    new_command.input.values = [
+        "My Application",  # formal name
+        "",  # app name - accept the default
+        "org.beeware",  # bundle ID
+        "My Project",  # project name
+        "Cool stuff",  # description
+        "Grace Hopper",  # author
+        "grace@navy.mil",  # author email
+        "https://navy.mil/myapplication",  # URL
+        "4",  # license
+        "4",  # PursuedPyBear GUI toolkit
+    ]
+
+    assert new_command.build_app_context() == {
+        "app_name": "myapplication",
+        "app_source": "\n"
+        "import os\n"
+        "import sys\n"
+        "\n"
+        "try:\n"
+        "    from importlib import metadata as importlib_metadata\n"
+        "except ImportError:\n"
+        "    # Backwards compatibility - importlib.metadata was added "
+        "in Python 3.8\n"
+        "    import importlib_metadata\n"
+        "\n"
+        "import ppb\n"
+        "\n"
+        "\n"
+        "class {{ cookiecutter.class_name }}(ppb.Scene):\n"
+        "    def __init__(self, **props):\n"
+        "        super().__init__(**props)\n"
+        "\n"
+        "        self.add(ppb.Sprite(\n"
+        "            image=ppb.Image('{{ cookiecutter.module_name "
+        "}}/resources/{{ cookiecutter.app_name }}.png'),\n"
+        "        ))\n"
+        "\n"
+        "\n"
+        "def main():\n"
+        "    # Linux desktop environments use app's .desktop file to "
+        "integrate the app\n"
+        "    # to their application menus. The .desktop file of this "
+        "app will include\n"
+        "    # StartupWMClass key, set to app's formal name, which "
+        "helps associate\n"
+        "    # app's windows to its menu item.\n"
+        "    #\n"
+        "    # For association to work any windows of the app must have "
+        "WMCLASS\n"
+        "    # property set to match the value set in app's desktop "
+        "file. For PPB this\n"
+        "    # is set using environment variable.\n"
+        "\n"
+        "    # Find the name of the module that was used to start the "
+        "app\n"
+        "    app_module = sys.modules['__main__'].__package__\n"
+        "    # Retrieve the app's metadata\n"
+        "    metadata = importlib_metadata.metadata(app_module)\n"
+        "\n"
+        "    os.environ['SDL_VIDEO_X11_WMCLASS'] = "
+        "metadata['Formal-Name']\n"
+        "\n"
+        "    ppb.run(\n"
+        "        starting_scene={{ cookiecutter.class_name }},\n"
+        "        title=metadata['Formal-Name'],\n"
+        "    )\n"
+        "        ",
+        "author": "Grace Hopper",
+        "author_email": "grace@navy.mil",
+        "bundle": "org.beeware",
+        "class_name": "MyApplication",
         "description": "Cool stuff",
+        "formal_name": "My Application",
+        "license": "GNU General Public License v2 (GPLv2)",
+        "linux_appimage_manylinux": "manylinux2014",
+        "linux_flatpak_runtime": "org.freedesktop.Platform",
+        "linux_flatpak_runtime_version": "22.08",
+        "linux_flatpak_sdk": "org.freedesktop.Sdk",
+        "module_name": "myapplication",
+        "project_name": "My Project",
+        "requires": '\n    "ppb~=1.1",\n',
+        "url": "https://navy.mil/myapplication",
+    }
+
+
+def test_question_sequence_pygame(new_command):
+    """Questions are asked, a context is constructed."""
+
+    # Prime answers for all the questions.
+    new_command.input.values = [
+        "My Application",  # formal name
+        "",  # app name - accept the default
+        "org.beeware",  # bundle ID
+        "My Project",  # project name
+        "Cool stuff",  # description
+        "Grace Hopper",  # author
+        "grace@navy.mil",  # author email
+        "https://navy.mil/myapplication",  # URL
+        "4",  # license
+        "5",  # Pygame GUI toolkit
+    ]
+
+    assert new_command.build_app_context() == {
+        "app_name": "myapplication",
+        "app_source": "\n"
+        "import os\n"
+        "import sys\n"
+        "\n"
+        "import pygame\n"
+        "\n"
+        "try:\n"
+        "    from importlib import metadata as importlib_metadata\n"
+        "except ImportError:\n"
+        "    # Backwards compatibility - importlib.metadata was added "
+        "in Python 3.8\n"
+        "    import importlib_metadata\n"
+        "\n"
+        "SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600\n"
+        "WHITE = (255, 255, 255)\n"
+        "\n"
+        "\n"
+        "def main():\n"
+        "    # Linux desktop environments use app's .desktop file to "
+        "integrate the app\n"
+        "    # to their application menus. The .desktop file of this "
+        "app will include\n"
+        "    # StartupWMClass key, set to app's formal name, which "
+        "helps associate\n"
+        "    # app's windows to its menu item.\n"
+        "    #\n"
+        "    # For association to work any windows of the app must have "
+        "WMCLASS\n"
+        "    # property set to match the value set in app's desktop "
+        "file. For PPB this\n"
+        "    # is set using environment variable.\n"
+        "\n"
+        "    # Find the name of the module that was used to start the "
+        "app\n"
+        '    app_module = sys.modules["__main__"].__package__\n'
+        "    # Retrieve the app's metadata\n"
+        "    metadata = importlib_metadata.metadata(app_module)\n"
+        "\n"
+        '    os.environ["SDL_VIDEO_X11_WMCLASS"] = '
+        'metadata["Formal-Name"]\n'
+        "\n"
+        "    pygame.init()\n"
+        '    pygame.display.set_caption(metadata["Formal-Name"])\n'
+        "    screen = pygame.display.set_mode((SCREEN_WIDTH, "
+        "SCREEN_HEIGHT))\n"
+        "\n"
+        "    running = True\n"
+        "    while running:\n"
+        "        for event in pygame.event.get():\n"
+        "            if event.type == pygame.QUIT:\n"
+        "                running = False\n"
+        "                break\n"
+        "\n"
+        "        screen.fill(WHITE)\n"
+        "        pygame.display.flip()\n"
+        "\n"
+        "    pygame.quit()\n"
+        "        ",
         "author": "Grace Hopper",
         "author_email": "grace@navy.mil",
+        "bundle": "org.beeware",
+        "class_name": "MyApplication",
+        "description": "Cool stuff",
+        "formal_name": "My Application",
+        "license": "GNU General Public License v2 (GPLv2)",
+        "linux_appimage_manylinux": "manylinux2014",
+        "linux_flatpak_runtime": "org.freedesktop.Platform",
+        "linux_flatpak_runtime_version": "22.08",
+        "linux_flatpak_sdk": "org.freedesktop.Sdk",
+        "module_name": "myapplication",
+        "project_name": "My Project",
+        "requires": '\n    "pygame~=2.2",\n',
         "url": "https://navy.mil/myapplication",
+    }
+
+
+def test_question_sequence_none(new_command):
+    """Questions are asked, a context is constructed."""
+
+    # Prime answers for all the questions.
+    new_command.input.values = [
+        "My Application",  # formal name
+        "",  # app name - accept the default
+        "org.beeware",  # bundle ID
+        "My Project",  # project name
+        "Cool stuff",  # description
+        "Grace Hopper",  # author
+        "grace@navy.mil",  # author email
+        "https://navy.mil/myapplication",  # URL
+        "4",  # license
+        "6",  # None
+    ]
+
+    assert new_command.build_app_context() == {
+        "app_name": "myapplication",
+        "author": "Grace Hopper",
+        "author_email": "grace@navy.mil",
+        "bundle": "org.beeware",
+        "class_name": "MyApplication",
+        "description": "Cool stuff",
+        "formal_name": "My Application",
         "license": "GNU General Public License v2 (GPLv2)",
-        "gui_framework": "PySide2",
+        "module_name": "myapplication",
+        "project_name": "My Project",
+        "url": "https://navy.mil/myapplication",
     }
 
 
@@ -71,16 +648,159 @@ def test_question_sequence_with_no_user_input(new_command):
     new_command.input.enabled = False
 
     assert new_command.build_app_context() == {
+        "android_requires": '\n    "toga-android~=0.4.0",\n',
+        "android_supported": "true",
         "app_name": "helloworld",
+        "app_source": "\n"
+        "import toga\n"
+        "from toga.style import Pack\n"
+        "from toga.style.pack import COLUMN, ROW\n"
+        "\n"
+        "\n"
+        "class {{ cookiecutter.class_name }}(toga.App):\n"
+        "\n"
+        "    def startup(self):\n"
+        '        """Construct and show the Toga application.\n'
+        "\n"
+        "        Usually, you would add your application to a main "
+        "content box. We then create a\n"
+        "        main window (with a name matching the app), and show "
+        "the main window.\n"
+        '        """\n'
+        "        main_box = toga.Box()\n"
+        "\n"
+        "        self.main_window = "
+        "toga.MainWindow(title=self.formal_name)\n"
+        "        self.main_window.content = main_box\n"
+        "        self.main_window.show()\n"
+        "\n"
+        "\n"
+        "def main():\n"
+        "    return {{ cookiecutter.class_name }}()\n",
         "author": "Jane Developer",
         "author_email": "jane@example.com",
         "bundle": "com.example",
         "class_name": "HelloWorld",
         "description": "My first application",
         "formal_name": "Hello World",
-        "gui_framework": "Toga",
+        "ios_requires": '\n    "toga-iOS~=0.4.0",\n    "std-nslog~=1.0.0",\n',
+        "ios_supported": "true",
         "license": "BSD license",
+        "linux_appimage_linuxdeploy_plugins": '\n    "DEPLOY_GTK_VERSION=3 gtk",\n',
+        "linux_appimage_manylinux": "manylinux2014",
+        "linux_appimage_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo-gobject-devel",\n'
+        "    # Needed to compile PyGObject wheel\n"
+        '    "gobject-introspection-devel",\n'
+        "    # Needed to provide GTK\n"
+        '    "gtk3-devel",\n'
+        "    # Dependencies that GTK looks for at "
+        "runtime, that need to be\n"
+        "    # in the build environment to be "
+        "picked up by linuxdeploy\n"
+        '    "libcanberra-gtk3",\n'
+        '    "PackageKit-gtk3-module",\n'
+        '    "gvfs-client",\n',
+        "linux_flatpak_runtime": "org.gnome.Platform",
+        "linux_flatpak_runtime_version": "44",
+        "linux_flatpak_sdk": "org.gnome.Sdk",
+        "linux_requires": '\n    "toga-gtk~=0.4.0",\n',
+        "linux_system_arch_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "gobject-introspection",\n'
+        "    # Runtime dependencies that need to "
+        "exist so that the\n"
+        "    # Arch package passes final "
+        "validation.\n"
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Dependencies that GTK looks for "
+        "at runtime\n"
+        '    "libcanberra",\n'
+        "    # Needed to provide WebKit2\n"
+        '    # "webkit2gtk",\n',
+        "linux_system_arch_system_runtime_requires": "\n"
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Needed to provide "
+        "PyGObject bindings\n"
+        "    "
+        '"gobject-introspection-runtime",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        '    "libcanberra",\n'
+        "    # Needed to provide WebKit2 "
+        "at runtime\n"
+        '    # "webkit2gtk",\n',
+        "linux_system_debian_system_requires": "\n"
+        "    # Needed to compile pycairo "
+        "wheel\n"
+        '    "libcairo2-dev",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "libgirepository1.0-dev",\n',
+        "linux_system_debian_system_runtime_requires": "\n"
+        "    # Needed to provide GTK "
+        "and its GI bindings\n"
+        '    "gir1.2-gtk-3.0",\n'
+        '    "libgirepository-1.0-1",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        "    "
+        '"libcanberra-gtk3-module",\n'
+        "    # Needed to provide "
+        "WebKit2 at runtime\n"
+        '    # "gir1.2-webkit2-4.0",\n',
+        "linux_system_rhel_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo-gobject-devel",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "gobject-introspection-devel",\n',
+        "linux_system_rhel_system_runtime_requires": "\n"
+        "    # Needed to support Python "
+        "bindings to GTK\n"
+        '    "gobject-introspection",\n'
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        '    "libcanberra-gtk3",\n'
+        "    # Needed to provide WebKit2 "
+        "at runtime\n"
+        '    # "webkit2gtk3",\n',
+        "linux_system_suse_system_requires": "\n"
+        "    # Needed to compile pycairo wheel\n"
+        '    "cairo-devel",\n'
+        "    # Needed to compile PyGObject "
+        "wheel\n"
+        '    "gobject-introspection-devel",\n',
+        "linux_system_suse_system_runtime_requires": "\n"
+        "    # Needed to provide GTK\n"
+        '    "gtk3",\n'
+        "    # Needed to support Python "
+        "bindings to GTK\n"
+        '    "gobject-introspection", '
+        '"typelib(Gtk)=3.0",\n'
+        "    # Dependencies that GTK "
+        "looks for at runtime\n"
+        '    "libcanberra-gtk3-0",\n'
+        "    # Needed to provide WebKit2 "
+        "at runtime\n"
+        '    # "libwebkit2gtk3",\n'
+        '    # "typelib(WebKit2)",\n',
+        "macos_requires": '\n    "toga-cocoa~=0.4.0",\n    "std-nslog~=1.0.0",\n',
+        "macos_universal_build": "true",
         "module_name": "helloworld",
         "project_name": "Hello World",
+        "start_app_source": "    main().main_loop()",
         "url": "https://example.com/helloworld",
+        "web_requires": '\n    "toga-web~=0.4.0",\n',
+        "web_style_framework": "Shoelace v2.3",
+        "web_supported": "true",
+        "windows_requires": '\n    "toga-winforms~=0.4.0",\n',
     }