diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c9d7e950..1d5d4456 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -181,6 +181,196 @@ jobs:
if-no-files-found: error
retention-days: 7
+ xbox-uwp-changes:
+ name: detect Xbox UWP changes
+ runs-on: ubuntu-latest
+ outputs:
+ changed: ${{ steps.paths.outputs.changed }}
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ - id: paths
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
+ HEAD_SHA: ${{ github.sha }}
+ run: |
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(ports/uwp/|scripts/build_xbox_uwp\.sh$|scripts/xbox-uwp/|scripts/pack_love\.sh$|\.github/workflows/(ci|release)\.yml$|src/core/Platform\.lua$|src/import/(CacheFs|LauncherView|RomImporter)\.lua$|src/update/Check\.lua$|tests/engine/(platform_nx|uwp_baseroms|uwp_native_picker)_test\.lua$|tests/rom_importer_double_pick_test\.lua$)'; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ xbox-uwp-selftest:
+ name: Xbox UWP offline selftest
+ needs: xbox-uwp-changes
+ if: needs.xbox-uwp-changes.outputs.changed == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - name: Xbox UWP offline selftest
+ run: bash scripts/xbox-uwp/selftest_build_xbox_uwp.sh
+ - name: Build shared payload
+ run: |
+ scripts/pack_love.sh \
+ --output .bazinga/work/ci-game.love \
+ --listing .bazinga/work/ci-love-listing.txt \
+ --version 0.0.0
+ - name: Upload shared payload
+ uses: actions/upload-artifact@v7
+ with:
+ name: gen1recomp-xbox-uwp-payload
+ path: .bazinga/work/ci-game.love
+ if-no-files-found: error
+ retention-days: 1
+
+ xbox-uwp-build:
+ name: Xbox UWP build
+ needs: [xbox-uwp-changes, xbox-uwp-selftest]
+ if: |
+ always()
+ && needs.xbox-uwp-changes.outputs.changed == 'true'
+ && needs.xbox-uwp-selftest.result == 'success'
+ runs-on: windows-2022
+ steps:
+ - uses: actions/checkout@v7
+ - name: Download shared payload
+ uses: actions/download-artifact@v8
+ with:
+ name: gen1recomp-xbox-uwp-payload
+ path: .bazinga/work
+ - name: Build Xbox UWP package
+ shell: bash
+ run: |
+ bash scripts/build_xbox_uwp.sh \
+ --release \
+ --version 0.0.0 \
+ --game-love .bazinga/work/ci-game.love
+ - name: Upload Xbox UWP package
+ uses: actions/upload-artifact@v7
+ with:
+ name: gen1recomp-xbox-uwp
+ path: |
+ dist/xbox-uwp/gen1recomp-0.0.0-xbox-uwp.zip
+ dist/xbox-uwp/gen1recomp-0.0.0-xbox-uwp.zip.sha256
+ if-no-files-found: error
+ retention-days: 7
+
+ linux-arm64-changes:
+ name: detect Linux arm64 changes
+ runs-on: ubuntu-latest
+ outputs:
+ changed: ${{ steps.paths.outputs.changed }}
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ - id: paths
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
+ HEAD_SHA: ${{ github.sha }}
+ run: |
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_linux_arm64\.sh$|scripts/linux-arm64/|scripts/pack_love\.sh$|docs/linux-arm64-build\.md$|\.github/workflows/(ci|release)\.yml$)'; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ linux-arm64-selftest:
+ name: Linux arm64 offline selftest
+ needs: linux-arm64-changes
+ if: needs.linux-arm64-changes.outputs.changed == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ # Deliberately on x86_64: everything this gate checks (pins, the
+ # host-arch guard, the dependency exclude list, the AppRun fusion
+ # contract) is answerable without a container or an aarch64 machine,
+ # so the slow native job below only ever starts on a sane tree.
+ - name: Linux arm64 offline selftest
+ run: bash scripts/linux-arm64/selftest_build_linux_arm64.sh
+
+ linux-arm64-build:
+ name: Linux arm64 AppImage build
+ needs: [linux-arm64-changes, linux-arm64-selftest]
+ if: |
+ always()
+ && needs.linux-arm64-changes.outputs.changed == 'true'
+ && needs.linux-arm64-selftest.result == 'success'
+ # No fork restriction, unlike switch-build: this needs no secrets and no
+ # self-hosted hardware, just GitHub's free arm64 runner for public repos,
+ # so contributors get the same coverage on their own PRs.
+ runs-on: ubuntu-24.04-arm
+ steps:
+ - uses: actions/checkout@v7
+ - name: Build the aarch64 AppImage
+ run: |
+ set -euo pipefail
+ scripts/build_linux_arm64.sh --version 0.0.0
+ - name: Verify the AppImage is self-contained and bullseye-compatible
+ run: |
+ set -euo pipefail
+ image="dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage"
+
+ # --appimage-extract needs no FUSE, so this works on a runner
+ # without /dev/fuse and still exercises the real payload.
+ "$image" --appimage-extract >/dev/null
+ for required in AppRun bin/love game.love lib/liblove-11.5.so; do
+ [ -e "squashfs-root/$required" ] \
+ || { echo "::error::AppImage is missing $required"; exit 1; }
+ done
+
+ # Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is
+ # applied; an unresolved soname here is a user-visible launch crash.
+ #
+ # This runs on a HEADLESS runner on purpose, and that is the point.
+ # The first version of this build bundled Debian's SDL2, which
+ # hard-links libpulse/libasound/libX11/libwayland, so it only ever
+ # started on a full desktop -- a bare runner is what exposed it.
+ missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \
+ ldd squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
+ | grep 'not found' || true)"
+ [ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; }
+
+ # Nothing may hard-link a driver, session or audio-stack library:
+ # those must be reached through dlopen so the AppImage runs on a box
+ # with only ALSA, only Wayland, or only KMSDRM.
+ linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; do
+ objdump -p "$f" 2>/dev/null | awk '/NEEDED/{print $2}'
+ done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo|sndio|dbus)' || true)"
+ [ -z "$linked" ] \
+ || { echo "::error::these must be dlopened, not linked:"; echo "$linked"; exit 1; }
+
+ # The whole point of compiling on bullseye. If a future change moves
+ # the builder to a newer base, the glibc floor silently rises and
+ # every user on an older distro gets "GLIBC_2.xx not found" -- catch
+ # it here instead of in a release.
+ floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
+ | grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)"
+ echo "highest required glibc symbol version: $floor"
+ [ -n "$floor" ] \
+ || { echo "::error::found no versioned glibc symbols -- objdump read nothing"; exit 1; }
+ highest="$(printf '%s\n' "$floor" "GLIBC_2.31" | sort -V | tail -1)"
+ [ "$highest" = "GLIBC_2.31" ] \
+ || { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; }
+ - name: Upload the AppImage
+ uses: actions/upload-artifact@v7
+ with:
+ name: gen1recomp-linux-arm64
+ path: |
+ dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage
+ dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage.sha256
+ if-no-files-found: error
+ retention-days: 7
+
headless:
name: headless suites (no ROM)
runs-on: ubuntu-latest
@@ -193,6 +383,9 @@ jobs:
- name: install luajit
run: sudo apt-get update && sudo apt-get install -y luajit
+ - name: install Pillow
+ run: python3 -m pip install --upgrade pillow
+
- name: interpreter version
run: luajit -v
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1671a324..c63c2576 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,8 +1,8 @@
name: Release
# Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS
-# IPA, a Nintendo Switch SD-ready zip (experimental), the Anbernic RG34XXSP
-# (Stock OS 64-bit MOD / PortMaster) and Linux ARM SBC PortMaster
+# IPA, a Nintendo Switch SD-ready zip (experimental), Xbox UWP, the Anbernic
+# RG34XXSP (Stock OS 64-bit MOD / PortMaster) and Linux ARM SBC PortMaster
# handheld ports on the self-hosted Mac runner, and publishes them as a
# GitHub Release.
#
@@ -44,22 +44,17 @@ concurrency:
cancel-in-progress: false
jobs:
- release:
- runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
-
+ version:
+ name: determine release version
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.ver.outputs.version }}
+ tag: ${{ steps.ver.outputs.tag }}
steps:
- # The self-hosted runner lives under the machine owner's home
- # directory; mask it first so absolute paths in every later step's
- # output show up as *** in the public workflow logs.
- - name: Mask runner paths
- run: echo "::add-mask::$HOME"
-
- - name: Checkout
- uses: actions/checkout@v7
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
-
- name: Determine version
id: ver
env:
@@ -67,7 +62,6 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
-
semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'
# 1) Explicit override from a manual run.
@@ -97,7 +91,6 @@ jobs:
| grep -E "$semver_re" \
| sort -t. -k1,1n -k2,2n -k3,3n \
| tail -1 || true)"
-
if [ -z "$latest" ]; then
version="0.1.0"
echo "No existing release tag; starting at $version"
@@ -125,10 +118,157 @@ jobs:
echo "::error::Release $tag already exists. Pick a different version."
exit 1
fi
-
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
+ love-payload:
+ name: build release game.love
+ needs: version
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - name: Build shared payload
+ run: |
+ scripts/pack_love.sh \
+ --output dist/payload/game.love \
+ --listing dist/payload/love-listing.txt \
+ --version "${{ needs.version.outputs.version }}"
+ - name: Upload shared payload
+ uses: actions/upload-artifact@v7
+ with:
+ name: gen1recomp-release-love
+ path: dist/payload/game.love
+ if-no-files-found: error
+ retention-days: 1
+
+ linux-arm64:
+ name: build Linux arm64 AppImage
+ needs: [version, love-payload]
+ # GitHub's free arm64 runner for public repos. It has to be arm64: the
+ # AppImage compiles LÖVE natively inside a Debian bullseye arm64
+ # container, and the qemu-emulated alternative takes hours.
+ runs-on: ubuntu-24.04-arm
+ steps:
+ - uses: actions/checkout@v7
+ - name: Download shared payload
+ uses: actions/download-artifact@v8
+ with:
+ name: gen1recomp-release-love
+ path: .bazinga/work
+ - name: Build Linux arm64 AppImage
+ run: |
+ set -euo pipefail
+ scripts/build_linux_arm64.sh \
+ --version "${{ needs.version.outputs.version }}" \
+ --game-love .bazinga/work/game.love
+ - name: Upload Linux arm64 release
+ uses: actions/upload-artifact@v7
+ with:
+ name: gen1recomp-linux-arm64-release
+ path: |
+ dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage
+ dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage.sha256
+ if-no-files-found: error
+ retention-days: 1
+
+ xbox-uwp:
+ name: build Xbox UWP release
+ needs: [version, love-payload]
+ runs-on: windows-2022
+ steps:
+ - uses: actions/checkout@v7
+ - name: Download shared payload
+ uses: actions/download-artifact@v8
+ with:
+ name: gen1recomp-release-love
+ path: .bazinga/work
+ - name: Prepare signing certificate
+ shell: pwsh
+ env:
+ CERTIFICATE_BASE64: ${{ secrets.XBOX_UWP_SIGNING_CERTIFICATE }}
+ CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
+ CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
+ run: |
+ if ($env:CANONICAL_REPOSITORY -eq 'true' -and
+ [string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
+ throw 'XBOX_UWP_SIGNING_CERTIFICATE is not configured.'
+ }
+ if ([string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
+ "UWP_PUBLISHER=CN=Gen1Recomp" | Out-File $env:GITHUB_ENV -Append
+ exit 0
+ }
+ $pfx = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.pfx'
+ [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:CERTIFICATE_BASE64))
+ $flags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
+ $cert = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
+ $pfx, $env:CERTIFICATE_PASSWORD, $flags)
+ $cer = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.cer'
+ [IO.File]::WriteAllBytes(
+ $cer,
+ $cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert))
+ Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople | Out-Null
+ "UWP_PFX=$pfx" | Out-File $env:GITHUB_ENV -Append
+ "UWP_CERT_THUMBPRINT=$($cert.Thumbprint)" | Out-File $env:GITHUB_ENV -Append
+ "UWP_PUBLISHER=$($cert.Subject)" | Out-File $env:GITHUB_ENV -Append
+ - name: Build Xbox UWP package
+ shell: bash
+ run: |
+ bash scripts/build_xbox_uwp.sh \
+ --release \
+ --version "${{ needs.version.outputs.version }}" \
+ --publisher "$UWP_PUBLISHER" \
+ --game-love .bazinga/work/game.love
+ - name: Sign and stage Xbox UWP release
+ shell: pwsh
+ env:
+ CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
+ run: |
+ if (-not $env:UWP_PFX) {
+ exit 0
+ }
+ scripts/xbox-uwp/stage_release.ps1 `
+ -Version '${{ needs.version.outputs.version }}' `
+ -Configuration Release `
+ -BuildInfo .bazinga/work/xbox-uwp-build-info.json `
+ -CertificatePath $env:UWP_PFX `
+ -CertificatePassword $env:CERTIFICATE_PASSWORD
+ - name: Upload Xbox UWP release
+ uses: actions/upload-artifact@v7
+ with:
+ name: gen1recomp-xbox-uwp-release
+ path: |
+ dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip
+ dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip.sha256
+ if-no-files-found: error
+ retention-days: 1
+ - name: Remove signing certificate
+ if: always()
+ shell: pwsh
+ run: |
+ if ($env:UWP_CERT_THUMBPRINT) {
+ Remove-Item "Cert:\LocalMachine\TrustedPeople\$env:UWP_CERT_THUMBPRINT" -ErrorAction SilentlyContinue
+ }
+ if ($env:UWP_PFX) {
+ Remove-Item $env:UWP_PFX -Force -ErrorAction SilentlyContinue
+ }
+
+ release:
+ needs: [version, xbox-uwp, linux-arm64]
+ runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
+
+ steps:
+ # The self-hosted runner lives under the machine owner's home
+ # directory; mask it first so absolute paths in every later step's
+ # output show up as *** in the public workflow logs.
+ - name: Mask runner paths
+ run: echo "::add-mask::$HOME"
+
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
- name: Import signing certificate into a temporary keychain
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
@@ -173,12 +313,12 @@ jobs:
# notarize separately below so it uses secret credentials, not a
# login-keychain profile. "all" also builds the Linux AppImage,
# which needs no signing/notarization.
- scripts/build.sh all --version "${{ steps.ver.outputs.version }}" --no-notarize
+ scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize
- name: Build Android
run: |
set -euo pipefail
- scripts/build_android.sh --version "${{ steps.ver.outputs.version }}"
+ scripts/build_android.sh --version "${{ needs.version.outputs.version }}"
- name: Install xcbeautify
run: |
@@ -192,10 +332,10 @@ jobs:
set -euo pipefail
if [ "$CANONICAL_REPOSITORY" = true ]; then
scripts/build_ios.sh --fetch --device --release \
- --version "${{ steps.ver.outputs.version }}"
+ --version "${{ needs.version.outputs.version }}"
else
scripts/build_ios.sh --fetch --release \
- --version "${{ steps.ver.outputs.version }}"
+ --version "${{ needs.version.outputs.version }}"
fi
- name: Build Switch
@@ -207,14 +347,14 @@ jobs:
# Needs native switch-tools (nacptool/elf2nro) and/or Docker on the
# Mac self-hosted runner; see docs/switch-build.md.
scripts/build_switch.sh --fetch --fused \
- --version "${{ steps.ver.outputs.version }}"
+ --version "${{ needs.version.outputs.version }}"
- name: Build Anbernic RG34XXSP port
run: |
set -euo pipefail
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
# runtime from PortMaster-GUI, so it needs no signing/notarization.
- ./build-rg34xxsp.sh --version "${{ steps.ver.outputs.version }}"
+ ./build-rg34xxsp.sh --version "${{ needs.version.outputs.version }}"
- name: Build Linux ARM SBC PortMaster port
env:
@@ -222,13 +362,13 @@ jobs:
# script defaults to the latest published release for standalone
# builds, while this explicit local override keeps CI source-aligned.
GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }}
- GEN1RECOMP_RELEASE_TAG: v${{ steps.ver.outputs.version }}
+ GEN1RECOMP_RELEASE_TAG: v${{ needs.version.outputs.version }}
run: |
set -euo pipefail
# Same aarch64 PortMaster-style pack for Linux ARM SBC PortMaster. The build
# keeps its own cache because the two scripts use different staging
# layouts and runtime package paths.
- ./build-linux-arm-sbc.sh --version "${{ steps.ver.outputs.version }}"
+ ./build-linux-arm-sbc.sh --version "${{ needs.version.outputs.version }}"
- name: Notarize & staple macOS app
if: github.repository == 'bryanthaboi/gen1recomp'
@@ -265,18 +405,41 @@ jobs:
ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"
echo "Notarized + stapled ✓"
+ - name: Download Xbox UWP release
+ if: github.repository == 'bryanthaboi/gen1recomp'
+ uses: actions/download-artifact@v8
+ with:
+ name: gen1recomp-xbox-uwp-release
+ path: dist/xbox-uwp
+
+ - name: Download Linux arm64 release
+ if: github.repository == 'bryanthaboi/gen1recomp'
+ uses: actions/download-artifact@v8
+ with:
+ name: gen1recomp-linux-arm64-release
+ path: dist/linux-arm64
+
- name: Stage release assets
if: github.repository == 'bryanthaboi/gen1recomp'
id: assets
run: |
set -euo pipefail
- v="${{ steps.ver.outputs.version }}"
+ v="${{ needs.version.outputs.version }}"
outdir="dist/release"
rm -rf "$outdir"
mkdir -p "$outdir"
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
+
+ # arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
+ # its own runner because LÖVE publishes no aarch64 binary and the
+ # AppImage has to be compiled natively; ships as a runnable
+ # AppImage rather than a zip so `chmod +x && ./it` just works.
+ arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
+ [ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
+ cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
+ chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
apk="$(find dist/android/debug -name '*.apk' | head -1)"
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
@@ -291,6 +454,10 @@ jobs:
# Local fused .nro stays under dist/switch/ for PR CI / debug; release
# publishes the SD-ready zip only.
+ uwp="dist/xbox-uwp/gen1recomp-${v}-xbox-uwp.zip"
+ [ -f "$uwp" ] || { echo "::error::$uwp not found (expected from the Xbox UWP job)"; exit 1; }
+ cp "$uwp" "$outdir/gen1recomp-${v}-xbox-uwp.zip"
+
# Anbernic handheld port (suffix names the CFW it targets, so a
# future RG35XX/other-CFW pack can ship alongside it).
rg34="dist/rg34xxsp/gen1recomp-rg34xxsp-stockos64-mod.zip"
@@ -322,8 +489,8 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
- v="${{ steps.ver.outputs.version }}"
- tag="${{ steps.ver.outputs.tag }}"
+ v="${{ needs.version.outputs.version }}"
+ tag="${{ needs.version.outputs.tag }}"
# Issues this release closes. Three sources, deduped by number:
# 1. GitHub's own "closing issues" links on every PR whose
@@ -411,9 +578,11 @@ jobs:
"dist/release/gen1recomp-${v}-macos.zip"
"dist/release/gen1recomp-${v}-windows.zip"
"dist/release/gen1recomp-${v}-linux.zip"
+ "dist/release/gen1recomp-${v}-linux-arm64.AppImage"
"dist/release/gen1recomp-${v}-android.apk"
"dist/release/gen1recomp-${v}-ios.ipa"
"dist/release/gen1recomp-${v}-switch.zip"
+ "dist/release/gen1recomp-${v}-xbox-uwp.zip"
"dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
"dist/release/gen1recomp-${v}-sbc-portmaster.zip"
"dist/release/gen1recomp-${v}.love"
@@ -432,7 +601,7 @@ jobs:
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
set -euo pipefail
- v="${{ steps.ver.outputs.version }}"
+ v="${{ needs.version.outputs.version }}"
ipa="dist/release/gen1recomp-${v}-ios.ipa"
app_repo="mobile/ios/app-repo.json"
[ -f "$ipa" ] || { echo "::error::$ipa not found"; exit 1; }
diff --git a/.gitignore b/.gitignore
index bc7f4683..d68dbada 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,12 +31,29 @@ mobile/ios/love-src/
mobile/ios/cache/
mobile/ios/build/
-# love-nx vendor binaries (fetch per docs/switch-development.md; also covered by .*)
+# love-nx vendor binaries (fetch per docs/switch-build.md; also covered by .*)
.bazinga/love-nx/
# Final packaged build artifacts (mac/win/web/android/ios/switch) — see scripts/build.sh
/dist/
+# Switch OTA launcher build outputs
+ports/switch/ota-launcher/build/
+ports/switch/ota-launcher/build-host/
+ports/switch/ota-launcher/*.nro
+ports/switch/ota-launcher/*.nacp
+ports/switch/ota-launcher/*.elf
+ports/switch/ota-launcher/*.map
+ports/switch/ota-launcher/romfs/logo.rgba
+ports/switch/ota-launcher/romfs/logo.png
+ports/switch/ota-launcher/romfs/cacert.pem
+ports/switch/ota-launcher/romfs/ota-bootstrap.nro
+ports/switch/ota-bootstrap/build/
+ports/switch/ota-bootstrap/*.nro
+ports/switch/ota-bootstrap/*.nacp
+ports/switch/ota-bootstrap/*.elf
+ports/switch/ota-bootstrap/*.map
+
# Legacy manual convenience-copy location (superseded by /dist/android/)
mobile/dist/
@@ -46,3 +63,8 @@ mobile/dist/
# per-machine iOS bundle-id pin (see scripts/build_ios.sh)
mobile/ios/bundle_id.local
+
+# Xbox UWP build output
+/ports/uwp/build/
+/ports/uwp/third_party/*/source/
+/ports/uwp/third_party/angle/depot_tools/
diff --git a/README.md b/README.md
index c1d4d08e..ba1fff43 100644
--- a/README.md
+++ b/README.md
@@ -205,6 +205,39 @@ even on a different computer, as long as the same folder comes along.
already written to either location is touched automatically, so copy files
over yourself if you want to carry existing progress across the switch.
+## Launch Options
+
+By default the app opens the launcher so you can pick a game. Launch options
+skip it and start one game directly, which is what you want for a one-click
+entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
+
+| Option | Effect |
+| --- | --- |
+| `--game=red` | boot Red, skipping the launcher (`blue` and `yellow` too, or just `r` / `b` / `y`) |
+| `--slot=2` | load that save slot; takes a slot number or a slot id |
+| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
+
+
+## Linux on arm64 (Raspberry Pi)
+
+Alongside the x86_64 `gen1recomp-*-linux.zip`, every release ships
+`gen1recomp-*-linux-arm64.AppImage` for 64-bit ARM desktop Linux — Raspberry
+Pi 4/5, Armbian and other SBC distros, and arm64 VMs on Apple Silicon:
+
+```sh
+chmod +x gen1recomp-*-linux-arm64.AppImage
+./gen1recomp-*-linux-arm64.AppImage
+```
+
+LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the
+engine — and SDL2, OpenAL and the codecs — from source inside a Debian
+bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and
+zlib on the host; OpenGL, X11, Wayland, KMSDRM, ALSA and PulseAudio are all
+dlopened, so the same image runs on a full desktop, a Wayland-only session or
+a KMSDRM handheld with no X server. Build instructions and the reasoning are
+in [docs/linux-arm64-build.md](docs/linux-arm64-build.md).
+
+
## iOS
Every release ships `gen1recomp-*-ios.ipa`. Sideload it with AltStore
@@ -222,6 +255,40 @@ build and install from source on a Mac instead, see
+## Xbox Dev Mode
+
+Every release ships `gen1recomp-*-xbox-uwp.zip` for Xbox One and Xbox Series
+consoles in Developer Mode. It cannot be installed in retail mode.
+
+Extract the archive, then use Xbox Device Portal to install the `.msix` and
+the x64 package under `Dependencies`.
+
+### External setup
+
+1. Put your legally obtained Red, Blue, or Yellow ROMs on an external drive.
+ Mod ZIPs can go on the same drive.
+2. Connect the drive to the Xbox and open Gen1Recomp.
+3. Select **Import ROM** or **Import Mod**, then choose the file with the Xbox
+ file picker.
+4. Repeat the ROM import for each version you want to use.
+
+### Internal setup
+
+1. Create a folder named `baseroms` on your PC and place your legally obtained
+ Red, Blue, or Yellow ROMs inside it.
+2. ZIP the folder, keeping `baseroms` at the top level of the archive.
+3. Launch Gen1Recomp once, then close it.
+4. Open Xbox Device Portal and upload the ZIP to
+ `Gen1Recomp/LocalState/pokemon-love2d/`.
+5. Choose **Yes** when Device Portal asks whether to extract the archive.
+6. Open Gen1Recomp. The launcher checks baseroms once at startup. When it finds a compatible ROM, that game’s tab shows ROM FOUND and an Import detected ROM button.
+
+ROMs, generated game data, saves, and mods remain in LocalState and are not
+included in the app.
+
+Source builds and package details are covered in
+[the Xbox UWP build notes](ports/uwp/BUILD.md).
+
## Handhelds
A PortMaster-style port for the **Anbernic RG34XXSP** on Stock OS 64-bit MOD
@@ -231,24 +298,18 @@ Install steps, controls, and troubleshooting live in
## Nintendo Switch
-Releases ship an SD-ready `gen1recomp-*-switch.zip` (issue
-[#531](https://github.com/bryanthaboi/gen1recomp/issues/531)). Runtime target
-is pinned [love-nx](https://github.com/retronx-team/love-nx) `11.5-nx1`.
-Requires a console that can run Switch homebrew. Hardware evidence: **OLED**
-(author) and **V1 / Erista** boot (community).
+Releases ship an SD-ready `gen1recomp-*-switch.zip`. Runtime target is pinned
+[love-nx](https://github.com/retronx-team/love-nx) `11.5-nx1`. Requires a
+console that can run Switch homebrew.
-- Players: [docs/switch-install.md](docs/switch-install.md) — download the
+- Players: [docs/switch-install.md](docs/switch-install.md). Download the
zip, extract at the microSD root (install or update), title-override
launch, import your own legal ROM, Joy-Con controls and shortcuts.
-- Builders: [docs/switch-build.md](docs/switch-build.md) — `--fetch` /
- `--loose` / `--fused`, toolchain, Docker fallback, and **CI vs release**
- (path-gated ubuntu selftest, canonical fused PR artifact, release hard-fail).
-
-Limitations, Dusklight-derived method, and how we tested:
-[docs/switch-development.md](docs/switch-development.md) and
-[docs/switch-hardware-evidence.md](docs/switch-hardware-evidence.md). Community
-help — especially HOS / love-nx packaging and broader hardware coverage — is
-welcome.
+- Builders: [docs/switch-build.md](docs/switch-build.md). `--fetch` /
+ `--loose` / `--fused`, toolchain, Docker fallback, and CI vs release
+ (path-gated ubuntu selftest, fused PR artifact on the main repo, release
+ hard-fail).
+- File transfer (MTP / SD / FTP): [docs/switch-transfer.md](docs/switch-transfer.md).
## Modding
@@ -297,7 +358,4 @@ This project would not be possible without [pret](https://github.com/pret) >
the pret band of decompiling maniacs > and their
[pokered](https://github.com/pret/pokered) disassembly.
-Nintendo Switch port: [andrewqsantos](https://github.com/andrewqsantos).
-Switch hardware testing (V1 boot): [booshankles](https://github.com/booshankles).
-

diff --git a/assets/fonts/plainpixel/PlainPixel-Regular.ttf b/assets/fonts/plainpixel/PlainPixel-Regular.ttf
new file mode 100644
index 00000000..873fe5f5
Binary files /dev/null and b/assets/fonts/plainpixel/PlainPixel-Regular.ttf differ
diff --git a/assets/fonts/plainpixel/README.md b/assets/fonts/plainpixel/README.md
new file mode 100644
index 00000000..22509351
--- /dev/null
+++ b/assets/fonts/plainpixel/README.md
@@ -0,0 +1,13 @@
+# Plain Pixel font
+
+"Plain Pixel Font" by Douglas Vautour (Burpy Fresh) is licensed under
+CC-BY 4.0: https://burpyfresh.itch.io
+
+Version 0.009 (CJK character additions), unmodified. Characters for most
+languages have a 5x11 base but can extend vertically; double-width
+characters such as Hiragana and Katakana are 11x11.
+
+Bundled so a translation mod can opt into TTF text rendering
+(`mod.content.font:register("ttf", {})`; see the Translation support
+section of docs/new-features.md) instead of drawing hundreds of glyph-page
+tiles. The tile font extracted from the player's ROM stays the default.
diff --git a/data/scripts/flavor/pewter_city.lua b/data/scripts/flavor/pewter_city.lua
index b5047092..eb525b7d 100644
--- a/data/scripts/flavor/pewter_city.lua
+++ b/data/scripts/flavor/pewter_city.lua
@@ -16,9 +16,13 @@ local function push(game, s, done)
game.stack:push(TextBox.new(game, s, done))
end
+-- PrintText on a text_end string returns with the box still drawn and
+-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
+-- engine/menus/text_box.asm); no A press clears the question first. Ride
+-- TextBox's opts.choice, the same as Commands.ask (#854).
local function ask(game, s, cb)
- local ChoiceBox = require("src.ui.ChoiceBox")
- push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
+ local TextBox = require("src.render.TextBox")
+ game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
end
M.PEWTER_CITY = {
diff --git a/data/scripts/flavor/viridian_city.lua b/data/scripts/flavor/viridian_city.lua
index b9e476da..f78626f8 100644
--- a/data/scripts/flavor/viridian_city.lua
+++ b/data/scripts/flavor/viridian_city.lua
@@ -23,9 +23,13 @@ local function push(game, s, done)
game.stack:push(TextBox.new(game, s, done))
end
+-- PrintText on a text_end string returns with the box still drawn and
+-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
+-- engine/menus/text_box.asm); no A press clears the question first. Ride
+-- TextBox's opts.choice, the same as Commands.ask (#854).
local function ask(game, s, cb)
- local ChoiceBox = require("src.ui.ChoiceBox")
- push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
+ local TextBox = require("src.render.TextBox")
+ game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
end
M.VIRIDIAN_CITY = {
diff --git a/data/scripts/gyms.lua b/data/scripts/gyms.lua
index c94484e3..ddf126c8 100644
--- a/data/scripts/gyms.lua
+++ b/data/scripts/gyms.lua
@@ -18,6 +18,26 @@ local M = {
VIRIDIAN_GYM = { city = "VIRIDIAN CITY", leader = "GIOVANNI", badge = "EARTHBADGE" },
}
+-- The originals' middle branch shared by every leader's text_asm: beaten
+-- but EVENT_GOT_TM* unset means the bag was full when the victory script
+-- ran GiveItem, so talking to the leader re-runs the ReceiveTM script.
+-- Returns true when the retry took over the talk. A save from before
+-- #797 already holds the TM without the flag; treat the owned TM as
+-- received so those saves fall through to the advice text instead of
+-- collecting a second copy.
+local function retryTmGive(game, ow, victoryKey, done)
+ local reward = require("data.scripts.victories")[victoryKey]
+ if not (reward and reward.gotFlag) then return false end
+ if game.save.flags[reward.gotFlag] then return false end
+ local owned = game.save.inventory and game.save.inventory[reward.item] or 0
+ if owned > 0 then
+ game.save.flags[reward.gotFlag] = true
+ return false
+ end
+ ow:offerGymTm(reward, done)
+ return true
+end
+
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
-- _PewterGymBrockPreBattleText and engages the leader battle
@@ -25,12 +45,14 @@ local M = {
-- badge/TM34 rewards and EVENT_BEAT_BROCK come from
-- data/scripts/victories.lua OPP_BROCK#1). After the badge his
-- .afterBeat branch prints _PewterGymBrockPostBattleAdviceText ("Go to
--- the GYM in CERULEAN..."). The original's middle branch (beat but
--- TM34 not yet handed over, CheckEventReuseA EVENT_GOT_TM34) is
--- unreachable in the port: the TM is granted with the victory.
+-- the GYM in CERULEAN..."). The middle branch (beat but TM34 not yet
+-- handed over, CheckEventReuseA EVENT_GOT_TM34 -> call
+-- PewterGymScriptReceiveTM34) retries the TM give when the bag was full
+-- at the victory (#797).
M.PEWTER_GYM.talk = {
TEXT_PEWTERGYM_BROCK = function(game, ow, npc, done)
if game.save.flags.EVENT_BEAT_BROCK then
+ if retryTmGive(game, ow, "OPP_BROCK#1", done) then return end
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text._PewterGymBrockPostBattleAdviceText
@@ -48,16 +70,17 @@ M.PEWTER_GYM.talk = {
-- (engageTrainer shows that same pre-battle text via resolveText; the
-- badge/TM rewards and the beat flag come from data/scripts/victories.lua)
-- -- and once beaten print the post-battle advice text. As with Brock,
--- the originals' middle branch (beaten but the TM not yet handed over,
--- CheckEventReuseA EVENT_GOT_TM*) is unreachable in the port: the TM is
--- granted with the victory.
+-- the middle branch (beaten but the TM not yet handed over,
+-- CheckEventReuseA EVENT_GOT_TM*) retries the TM give when the bag was
+-- full at the victory.
-- afterAdvice, when given, takes over `done`: it is handed (game, ow, npc,
-- done) and must call done() itself once whatever it's doing (e.g. a fade
-- around a HideObject) finishes, rather than having it invoked
-- automatically. Only Giovanni's farewell uses this.
-local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice)
+local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice, victoryKey)
return function(game, ow, npc, done)
if game.save.flags[beatFlag] then
+ if victoryKey and retryTmGive(game, ow, victoryKey, done) then return end
local TextBox = require("src.render.TextBox")
local finish = done
if afterAdvice then
@@ -79,42 +102,42 @@ end
M.CERULEAN_GYM.talk = {
TEXT_CERULEANGYM_MISTY = leaderTalk("EVENT_BEAT_MISTY",
"_CeruleanGymMistyTM11ExplanationText",
- "TM11 teaches\nBUBBLEBEAM!"),
+ "TM11 teaches\nBUBBLEBEAM!", nil, "OPP_MISTY#1"),
}
-- scripts/VermilionGym.asm VermilionGymLTSurgeText .got_tm24_already
M.VERMILION_GYM.talk = {
TEXT_VERMILIONGYM_LT_SURGE = leaderTalk("EVENT_BEAT_LT_SURGE",
"_VermilionGymLTSurgePostBattleAdviceText",
- "A little word of\nadvice, kid!"),
+ "A little word of\nadvice, kid!", nil, "OPP_LT_SURGE#1"),
}
-- scripts/CeladonGym.asm CeladonGymErikaText .afterBeat
M.CELADON_GYM.talk = {
TEXT_CELADONGYM_ERIKA = leaderTalk("EVENT_BEAT_ERIKA",
"_CeladonGymErikaPostBattleAdviceText",
- "You are cataloging\nPOKéMON? I must\nsay I'm impressed."),
+ "You are cataloging\nPOKéMON? I must\nsay I'm impressed.", nil, "OPP_ERIKA#1"),
}
-- scripts/FuchsiaGym.asm FuchsiaGymKogaText .afterBeat
M.FUCHSIA_GYM.talk = {
TEXT_FUCHSIAGYM_KOGA = leaderTalk("EVENT_BEAT_KOGA",
"_FuchsiaGymKogaPostBattleAdviceText",
- "When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!"),
+ "When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!", nil, "OPP_KOGA#1"),
}
-- scripts/SaffronGym.asm SaffronGymSabrinaText .afterBeat
M.SAFFRON_GYM.talk = {
TEXT_SAFFRONGYM_SABRINA = leaderTalk("EVENT_BEAT_SABRINA",
"_SaffronGymSabrinaPostBattleAdviceText",
- "Everyone has\npsychic power!\nPeople just don't\nrealize it!"),
+ "Everyone has\npsychic power!\nPeople just don't\nrealize it!", nil, "OPP_SABRINA#1"),
}
-- scripts/CinnabarGym.asm CinnabarGymBlaineText .afterBeat
M.CINNABAR_GYM.talk = {
TEXT_CINNABARGYM_BLAINE = leaderTalk("EVENT_BEAT_BLAINE",
"_CinnabarGymBlainePostBattleAdviceText",
- "FIRE BLAST is the\nultimate fire\ntechnique!"),
+ "FIRE BLAST is the\nultimate fire\ntechnique!", nil, "OPP_BLAINE#1"),
}
-- scripts/ViridianGym.asm ViridianGymGiovanniText .afterBeat: after the
@@ -142,7 +165,7 @@ M.VIRIDIAN_GYM.talk = {
"VIRIDIAN_GYM", "VIRIDIANGYM_GIOVANNI")
end
end, done))
- end),
+ end, "OPP_GIOVANNI#3"),
}
return M
diff --git a/data/scripts/oaks_lab_yellow.lua b/data/scripts/oaks_lab_yellow.lua
index 8f4ac856..c56f6091 100644
--- a/data/scripts/oaks_lab_yellow.lua
+++ b/data/scripts/oaks_lab_yellow.lua
@@ -45,6 +45,8 @@ return {
{ "stop_music" },
{ "play_music", "Music_MeetRival" },
{ "show_text", "_OaksLabRivalGrampsText" },
+ -- callfar OaksLabPikachuMovementScript, before ShowObject (#1021)
+ { "pikachu_make_way" },
{ "show_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
{ "place_npc", RIVAL, 4, 7, "up" },
{ "move_npc_to", RIVAL, 4, 3 },
@@ -192,12 +194,13 @@ return {
end
rows[#rows + 1] = { "face_player_dir", "up" }
rows[#rows + 1] = { "face_object", OAK1, "down" }
- -- OaksLabPlayerReceivedMonText: no nickname prompt -- the starter
- -- Pikachu keeps its species name
+ -- OaksLabPlayerReceivedMonText clears wMonDataLocation, so AskName runs (#1013)
rows[#rows + 1] = { "show_text", "_OaksLabOakGivesText" }
rows[#rows + 1] = { "play_sound", "Get_Key_Item" }
rows[#rows + 1] = { "show_text", "_OaksLabReceivedText", { RAM = "PIKACHU" } }
- rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5, true }
+ rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5 }
+ -- DisablePikachuOverworldSpriteDrawing keeps it in the ball (#1009)
+ rows[#rows + 1] = { "set_field", "pikachuInBall", true }
rows[#rows + 1] = { "set_flag", "EVENT_GOT_STARTER" }
rows[#rows + 1] = { "set_flag", "EVENT_CHOSE_PIKACHU" }
ow.runner:run(rows, { npc = npc, onDone = done })
@@ -296,9 +299,9 @@ return {
table.insert(rows, { "move_npc_to", RIVAL, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
table.insert(rows, { "play_music", "Music_OaksLab" })
- -- OaksLabPikachuEscapesPokeballScript: Pikachu hates its ball.
- -- The overworld follower itself is still an open port
- -- (docs/yellow-version.md runtime backlog); the story beat plays.
+ -- OaksLabPikachuEscapesPokeballScript: the follower reaches the map (#1009)
+ table.insert(rows, { "face_player_dir", "up" })
+ table.insert(rows, { "set_field", "pikachuInBall", false })
table.insert(rows, { "play_cry", "PIKACHU" })
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" })
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" })
diff --git a/data/scripts/story.lua b/data/scripts/story.lua
index 8bcabcd0..61eb95df 100644
--- a/data/scripts/story.lua
+++ b/data/scripts/story.lua
@@ -800,9 +800,13 @@ M.SILPH_CO_11F = {
-- line) would touch, and the whole Silph ending -- the flag, the Master
-- Ball, the Saffron streets clearing -- silently never happened.
--
- -- engageTrainer shows TEXT_SILPHCO11F_GIOVANNI as the battle text and,
- -- via victories.lua OPP_GIOVANNI#2, sets the event on a win; a loss
- -- sets nothing, so the trigger re-arms exactly as vanilla does.
+ -- SilphCo11FDefaultScript orders it DisplayTextID TEXT_SILPHCO11F_GIOVANNI
+ -- FIRST, then MoveSprite .GiovanniMovement: he speaks from behind the desk
+ -- and only then walks the three tiles down. Moving him before the box made
+ -- him cross the room in silence and deliver the speech point-blank (#869),
+ -- so the box comes first here and engageTrainer skips its own battle text.
+ -- victories.lua OPP_GIOVANNI#2 sets the event on a win; a loss sets
+ -- nothing, so the trigger re-arms exactly as vanilla does.
onStep = function(game, ow, x, y)
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then return false end
if not ((x == 6 and y == 13) or (x == 7 and y == 12)) then return false end
@@ -811,21 +815,28 @@ M.SILPH_CO_11F = {
if npc.def and npc.def.name == "SILPHCO11F_GIOVANNI" then gio = npc break end
end
if not gio or ow:trainerDefeated(gio) then return false end
- ow:scriptMove(gio, "down", 3, function()
- gio:facePlayer(ow.player)
- ow:engageTrainer(gio, function()
- -- SilphCo11FGiovanniAfterBattleScript: the "Blast it all!" speech,
- -- then SilphCo11FTeamRocketLeavesScript behind a fade so every Silph
- -- rocket leaves off-screen (the street rockets are handled by
- -- M.SAFFRON_CITY.onEnter in story4.lua). Queued, not run here: the
- -- battle's own callbacks are still unwinding, so queueScript starts
- -- it on the first idle overworld frame -- after the end-battle
- -- "Arrgh!!" box victories.lua OPP_GIOVANNI#2 pushes (#722).
- if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
- ow:queueScript(silphAftermathRows())
- end
- end)
- end)
+ local TextBox = require("src.render.TextBox")
+ game.stack:push(TextBox.new(game,
+ game.data.text._SilphCo11FGiovanniText
+ or "Ah {PLAYER}!\nSo we meet again!",
+ function()
+ ow:scriptMove(gio, "down", 3, function()
+ gio:facePlayer(ow.player)
+ ow:engageTrainer(gio, function()
+ -- SilphCo11FGiovanniAfterBattleScript: the "Blast it all!"
+ -- speech, then SilphCo11FTeamRocketLeavesScript behind a fade so
+ -- every Silph rocket leaves off-screen (the street rockets are
+ -- handled by M.SAFFRON_CITY.onEnter in story4.lua). Queued, not
+ -- run here: the battle's own callbacks are still unwinding, so
+ -- queueScript starts it on the first idle overworld frame --
+ -- after the end-battle "Arrgh!!" box victories.lua OPP_GIOVANNI#2
+ -- pushes (#722).
+ if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
+ ow:queueScript(silphAftermathRows())
+ end
+ end, nil, true)
+ end)
+ end))
return true
end,
onEnter = function(game, ow)
@@ -1031,38 +1042,43 @@ local championsRoomRivalScript = {
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 10
-- ChampionsRoomOakArrivesScript: Music_Cities1AlternateTempo
-- (Cities1, kept into HALL_OF_FAME like BIT_NO_MAP_MUSIC after
- -- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in
- { "play_music", "Music_Cities1", { keep = true } }, -- 11
- { "show_text", "_ChampionsRoomOakText" }, -- 12
- { "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 13
- { "move_npc", 2, "up", 5 }, -- 14 OakEntranceAfterVictoryMovement
+ -- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in.
+ -- audio/alternate_tempo.asm Music_Cities1AlternateTempo is not a plain
+ -- PlayMusic: it fades the current song out (wAudioFadeOutControl = 10),
+ -- waits 100 frames for the fade, then restarts Cities1 with channel 1
+ -- pointed at Music_Cities1_Ch1_AlternateTempo -- `tempo 232` where the
+ -- normal Music_Cities1_Ch1 opens `tempo 144`, i.e. the slower, heavier
+ -- reading of the town theme this scene is known for (#847).
+ { "fade_music", 10 }, -- 11
+ { "wait", 100 }, -- 12
+ { "play_music", "Music_Cities1", { keep = true, tempo = 232 } }, -- 13
+ { "show_text", "_ChampionsRoomOakText" }, -- 14
+ { "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 15
+ { "move_npc", 2, "up", 5 }, -- 16 OakEntranceAfterVictoryMovement
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
- { "face_object", 1, "left" }, -- 15
- { "face_object", 2, "down" }, -- 16
- { "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 17
+ { "face_object", 1, "left" }, -- 17
+ { "face_object", 2, "down" }, -- 18
+ { "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 19
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
- { "face_object", 2, "right" }, -- 18
- { "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 19
+ { "face_object", 2, "right" }, -- 20
+ { "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 21
-- OakComeWithMeScript: Oak faces down again, then exits up
- { "face_object", 2, "down" }, -- 20
- { "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 21
- { "move_npc", 2, "up", 2 }, -- 22 OakExitChampionsRoomMovement
- { "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 23
- -- ChampionsRoomPlayerFollowsOakScript / WalkToHallOfFame_RLEMovement
- -- (PAD_UP 4, PAD_LEFT 1): the player walks out after Oak instead of the
- -- screen just fading on the spot (#704). The entrance walk leaves the
- -- player at (4,3) and both north-wall warps sit on row 0, so the original
- -- only ever spends three of those simulated steps -- CheckWarpsNoCollision
- -- takes the HALL_OF_FAME warp the moment the walk lands on (4,0) and the
- -- trailing UP/LEFT are dropped. Scripted steps ignore collision here just
- -- as they do in the original (CollisionCheckOnLand skips its checks while
- -- wSimulatedJoypadStatesIndex is non-zero), so stepping through the
- -- rival's cell at (4,2) is the ported behavior, not a clip.
- { "move_player", "up", 3 }, -- 24
+ { "face_object", 2, "down" }, -- 22
+ { "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 23
+ { "move_npc", 2, "up", 2 }, -- 24 OakExitChampionsRoomMovement
+ { "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 25
+ -- ChampionsRoomPlayerFollowsOakScript / WalkToHallOfFame_RLEMovement.
+ -- The player walks out after Oak instead of the screen just fading on the
+ -- spot (#704). Route one tile right before walking north so the player
+ -- reaches the north-wall HALL_OF_FAME warp without sharing the rival's
+ -- (4,2) cell. The original simulated movement bypasses entity collision,
+ -- but this scene should not visibly walk through the defeated rival.
+ { "move_player", "right", 1 }, -- 26
+ { "move_player", "up", 3 }, -- 27
-- hand the induction off to the HALL_OF_FAME room (consumed by its
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
- { "set_field", "pendingHallOfFame", true }, -- 25
- { "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 26
+ { "set_field", "pendingHallOfFame", true }, -- 28
+ { "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 29
}
M.CHAMPIONS_ROOM = {
diff --git a/data/scripts/story2.lua b/data/scripts/story2.lua
index f38b0af6..b7e40f3d 100644
--- a/data/scripts/story2.lua
+++ b/data/scripts/story2.lua
@@ -400,15 +400,24 @@ M.POKEMON_FAN_CLUB = {
TEXT_POKEMONFANCLUB_CHAIRMAN = {
{ "face_player" }, -- 1
{ "check_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 2
- { "jump_if_true", 9 }, -- 3
- { "show_text", "_PokemonFanClubChairmanIntroText" }, -- 4
- { "show_text", "_PokemonFanClubChairmanStoryText" }, -- 5
+ { "jump_if_true", "nothing_left" }, -- 3
+ -- YesNoChoice (scripts/PokemonFanClub.asm): NO forfeits the voucher (#1050)
+ { "ask", "_PokemonFanClubChairmanIntroText" }, -- 4
+ { "jump_if_false", "no_story" }, -- 5
+ { "show_text", "_PokemonFanClubChairmanStoryText" }, -- 6
-- give-then-print like scripts/PokemonFanClub.asm (GiveItem
-- fills wStringBuffer; the received text reads it)
- { "give_item", "BIKE_VOUCHER", 1, false }, -- 6
- { "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 7
- { "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 8
- { "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 9
+ { "give_item", "BIKE_VOUCHER", 1, false }, -- 7
+ { "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 8
+ { "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 9
+ { "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 10
+ { "jump", "end" }, -- 11
+ { "label", "no_story" }, -- 12
+ { "show_text", "_PokemonFanClubNoStoryText" }, -- 13
+ { "jump", "end" }, -- 14
+ -- .nothingleft: the gift is done, he only reminisces now
+ { "label", "nothing_left" }, -- 15
+ { "show_text", "_PokemonFanClubChairFinalText" }, -- 16
},
},
}
diff --git a/data/scripts/story3.lua b/data/scripts/story3.lua
index 56567a50..4f0c507e 100644
--- a/data/scripts/story3.lua
+++ b/data/scripts/story3.lua
@@ -151,9 +151,27 @@ M.POKEMON_TOWER_6F = {
-- trick, and the speedrun route this bot follows depends on it.
if result == "win" or battle.pokeDollEscape then
game.save.flags.EVENT_BEAT_GHOST_MAROWAK = true
- game.stack:push(TextBox.new(game,
- t._PokemonTower6FSoulWasCalmedText
- or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!"))
+ -- PokemonTower6FMarowakDepartedText (scripts/PokemonTower6F.asm)
+ -- is two texts, not one: the CUBONE's-mother line first, then
+ -- PlayCry RESTLESS_SOUL (EQU MAROWAK, constants/pokemon_constants
+ -- .asm:209) + WaitForSoundToFinish + DelayFrames 30 before the
+ -- calmed line; the port dropped the first text and the cry
+ -- (#867). play_cry arms the next show_text, so the cry rides
+ -- the calmed box's open with the button prompt kept, and the
+ -- wait row stands in for the asm's 30-frame gap.
+ local rows = {
+ { "show_text", t._PokemonTower6FGhostWasCubonesMotherText
+ or "The GHOST was the\nrestless soul of\vCUBONE's mother!" },
+ { "play_cry", "MAROWAK", true },
+ { "wait", 30 },
+ { "show_text", t._PokemonTower6FSoulWasCalmedText
+ or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!" },
+ }
+ if ow.runner then
+ ow.runner:run(rows)
+ elseif ow.queueScript then
+ ow:queueScript(rows)
+ end
elseif result ~= "lose" then
-- .did_not_defeat: one simulated step right, off the trigger,
-- so fleeing does not leave you standing on a cell that
@@ -517,6 +535,14 @@ M.GAME_CORNER = {
done()
return
end
+ -- GameCornerRocketText hands the battle its own loss line through
+ -- SaveEndBattleTextPointers (.BattleEndText ->
+ -- _GameCornerRocketBattleEndText, "Dang!"), and PrintEndBattleText
+ -- prints it ON the battle screen between TrainerDefeatedText and
+ -- MoneyForWinningText (engine/battle/core.asm TrainerBattleVictory).
+ -- He is a text_asm trainer with no def_trainers header, so there is no
+ -- header.won for engageTrainer to find and the line has to be handed
+ -- over here or it never shows at all (#862).
ow:engageTrainer(npc, function()
if not ow:trainerDefeated(npc) then
done()
@@ -527,19 +553,44 @@ M.GAME_CORNER = {
game.data.text._GameCornerRocketAfterBattleText
or "Our hideout might\nbe discovered! I\nbetter tell BOSS!",
function()
- -- #198: GameCornerRocketExitScript (scripts/GameCorner.asm)
- -- ApplyMovementData walks the grunt one tile UP into the poster
- -- (the hideout's secret entrance at 9,4) before HideObject, so
- -- he leaves the floor rather than popping out of existence on
- -- (9,5). scriptMove locks player input (#scriptMoves>0) and
- -- ignores collision, so we despawn + unfreeze (done) only once
- -- the step lands.
- ow:scriptMove(npc, "up", 1, function()
- hideRocket()
- done()
- end)
+ -- #198/#862: GameCornerRocketBattleScript (scripts/GameCorner.asm)
+ -- picks the exit walk from where the player is standing, because
+ -- the grunt on (9,5) has to get past him: wYCoord == 6 (talked to
+ -- from the south) or wXCoord == 8 (from the west) leaves the row
+ -- clear and takes GameCornerMovement_Rocket_WalkDirect, five steps
+ -- RIGHT; otherwise the player is east of him on (10,5) and
+ -- GameCornerMovement_Rocket_WalkAroundPlayer steps DOWN, right, UP
+ -- and right again to go AROUND him. pokeyellow's copy of the
+ -- around-path takes one extra RIGHT on the lower row before coming
+ -- back up (it also has to clear Pikachu); both versions end on
+ -- (15,5). He never steps UP: (9,4) is the poster wall, which is
+ -- where the old single UP step sent him.
+ local px = ow.player and ow.player.cellX
+ local py = ow.player and ow.player.cellY
+ local path
+ if py == 6 or px == 8 then
+ path = { { "right", 5 } }
+ elseif require("src.core.GameVersion").isYellow() then
+ path = { { "down", 1 }, { "right", 3 }, { "up", 1 }, { "right", 3 } }
+ else
+ path = { { "down", 1 }, { "right", 2 }, { "up", 1 }, { "right", 4 } }
+ end
+ -- GameCornerRocketExitScript only HideObjects him once
+ -- BIT_SCRIPTED_NPC_MOVEMENT clears, i.e. after the last step.
+ -- scriptMove locks player input (#scriptMoves>0) and ignores
+ -- collision, so the despawn + unfreeze (done) ride the final step.
+ local function step(i)
+ if i > #path then
+ hideRocket()
+ done()
+ return
+ end
+ ow:scriptMove(npc, path[i][1], path[i][2],
+ function() step(i + 1) end)
+ end
+ step(1)
end))
- end)
+ end, game.data.text._GameCornerRocketBattleEndText or "Dang!")
end,
-- GameCornerClerk1Text (scripts/GameCorner.asm): the offer, a
-- YesNoChoice, then ¥1000 for 50 coins. Yellow drops the "1" from the
diff --git a/data/scripts/story4.lua b/data/scripts/story4.lua
index 874a2b9c..b4766406 100644
--- a/data/scripts/story4.lua
+++ b/data/scripts/story4.lua
@@ -13,16 +13,30 @@ local function push(game, s, done)
game.stack:push(TextBox.new(game, s, done))
end
+-- The question stays on screen under the YES/NO menu. The dojo prize
+-- balls are the clearest case: FightingDojoHitmonleePokeBallText
+-- (scripts/FightingDojo.asm) is `call PrintText` on a text_end string --
+-- no prompt, so no WaitForTextScrollButtonPress -- immediately followed
+-- by `call YesNoChoice`, and InitYesNoTextBoxParameters
+-- (engine/menus/text_box.asm) puts the menu above the dialogue box
+-- rather than replacing it. Ride TextBox's opts.choice, the same as
+-- Commands.ask, instead of popping the box with an A press and leaving a
+-- bare ChoiceBox over the overworld (#854).
local function ask(game, s, cb)
- local ChoiceBox = require("src.ui.ChoiceBox")
- push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
+ local TextBox = require("src.render.TextBox")
+ game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
end
--- fill the extracted text placeholders ({NUM:...}, {RAM:...}, {PLAYER})
+-- fill text placeholders; key on the hram/wram symbol first, since one
+-- string can carry two different NUM slots (#1006)
local function fill(s, subs)
s = s:gsub("{PLAYER}", subs.player or "")
- s = s:gsub("{NUM:[^}]*}", function() return tostring(subs.num or "") end)
- s = s:gsub("{RAM:[^}]*}", function() return subs.ram or "" end)
+ s = s:gsub("{NUM:([%w_]*)[^}]*}", function(name)
+ return tostring(subs[name] or subs.num or "")
+ end)
+ s = s:gsub("{RAM:([%w_]*)[^}]*}", function(name)
+ return subs[name] or subs.ram or ""
+ end)
return s
end
@@ -73,9 +87,12 @@ local function oaksAide(threshold, itemId, repeatText)
{ ram = itemName, player = game.save.player.name }), done)
end)
else
+ -- .notEnoughOwnedMons prints owned then requirement, two counts
push(game, fill(t._OaksAideUhOhText or
"You have only\ncaught {NUM:}!",
- { num = owned, ram = itemName }), done)
+ { num = owned, ram = itemName,
+ hOaksAideNumMonsOwned = owned,
+ hOaksAideRequirement = threshold }), done)
end
end)
end
@@ -155,19 +172,26 @@ local function dojoBall(species, ownBall, otherBall, askKey)
push(game, "You'll have to\nbeat the master\nfirst!", done)
return
end
- ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes)
- if not yes then done() return end
- flags["EVENT_GOT_" .. species] = true
- flags.EVENT_DEFEATED_FIGHTING_DOJO = true
- local Commands = require("src.script.Commands")
- local ctx = { save = game.save, game = game, overworld = ow }
- Commands.give_pokemon(ctx, species, 30)
- -- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides
- -- just the picked object's index) and routes to the greedy line above
- -- when talked to (#197).
- Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall)
- push(game, ("%s got\n%s!"):format(game.save.player.name, species), done)
- end)
+ -- Examining a ball shows that species' POKéDEX entry first
+ -- (DisplayPokedex in FightingDojo.asm, which also marks it seen),
+ -- then the yes/no take-it prompt (#853).
+ local Commands = require("src.script.Commands")
+ local ctx = { save = game.save, game = game, overworld = ow }
+ Commands.mark_seen(ctx, species)
+ local DexEntryMenu = require("src.ui.DexEntryMenu")
+ game.stack:push(DexEntryMenu.new(game, species, function()
+ ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes)
+ if not yes then done() return end
+ flags["EVENT_GOT_" .. species] = true
+ flags.EVENT_DEFEATED_FIGHTING_DOJO = true
+ Commands.give_pokemon(ctx, species, 30)
+ -- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides
+ -- just the picked object's index) and routes to the greedy line above
+ -- when talked to (#197).
+ Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall)
+ push(game, ("%s got\n%s!"):format(game.save.player.name, species), done)
+ end)
+ end))
end
end
@@ -229,27 +253,32 @@ M.FIGHTING_DOJO = {
M.SILPH_CO_7F = {
talk = {
- TEXT_SILPHCO7F_SILPH_WORKER_M1 = function(game, ow, npc, done)
- local t = text(game)
- if game.save.flags.EVENT_GOT_LAPRAS then
- push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
- or "How is LAPRAS\ndoing?", done)
- return
- end
- push(game, t._SilphCo7FSilphWorkerM1ThankYouText
- or "Thank you for\nsaving us!\fI want you to\nhave this LAPRAS!",
- function()
- game.save.flags.EVENT_GOT_LAPRAS = true
- local Commands = require("src.script.Commands")
- Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
- "LAPRAS", 15)
- push(game, ("%s got\nLAPRAS!"):format(game.save.player.name),
- function()
- push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
- or "It's a good\nswimmer!", done)
- end)
- end)
- end,
+ -- command rows, not a Lua handler: give_pokemon needs a runner to AskName (#1049)
+ TEXT_SILPHCO7F_SILPH_WORKER_M1 = {
+ { "face_player" },
+ { "check_flag", "EVENT_GOT_LAPRAS" },
+ { "jump_if_true", "has_lapras" },
+ { "show_text", "_SilphCo7FSilphWorkerM1HaveThisPokemonText" },
+ { "give_pokemon", "LAPRAS", 15 },
+ { "jump_if_false", "box_full" },
+ -- flag ahead of the jingle, like the Celadon EEVEE (#426)
+ { "set_flag", "EVENT_GOT_LAPRAS" },
+ { "play_sound", "Get_Item1" },
+ { "show_text", "_GotMonText", { RAM = "LAPRAS" } },
+ { "show_text", "_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
+ { "jump", "end" },
+ { "label", "box_full" },
+ { "show_text", "_BoxIsFullText" },
+ { "jump", "end" },
+ -- SilphCo7F.asm .saved_silph gates the thanks on Giovanni
+ { "label", "has_lapras" },
+ { "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
+ { "jump_if_true", "saved" },
+ { "show_text", "_SilphCo7FSilphWorkerM1IsOurPresidentOkText" },
+ { "jump", "end" },
+ { "label", "saved" },
+ { "show_text", "_SilphCo7FSilphWorkerM1SavedText" },
+ },
},
}
diff --git a/data/scripts/story5.lua b/data/scripts/story5.lua
index 89324f90..b1a7ffc8 100644
--- a/data/scripts/story5.lua
+++ b/data/scripts/story5.lua
@@ -49,7 +49,7 @@ local function gift(opts)
end
end)
end
- if opts.pre then say(opts.pre, "", give) else give() end
+ if opts.pre then say(opts.pre, opts.preFallback or "", give) else give() end
end
end
@@ -118,11 +118,21 @@ M.CINNABAR_LAB_METRONOME_ROOM = {
},
}
--- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher; no pre text)
+-- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher). The fisher's
+-- YouCanHaveThisText prints before GiveItem, so this gift needs a pre
+-- text (#775). Like the SilphCo2F worker (#393) that label carries no
+-- leading underscore, and on Red it sits outside the extractor's symbol
+-- set, so the literal from text/ViridianCity.asm rides along as the
+-- fallback; Yellow resolves the ROM string instead.
M.VIRIDIAN_CITY = {
talk = {
TEXT_VIRIDIANCITY_FISHER = gift({
flag = "EVENT_GOT_TM42", item = "TM_DREAM_EATER",
+ pre = "ViridianCityFisherYouCanHaveThisText",
+ preFallback = "Yawn!\nI must have dozed\voff in the sun."
+ .. "\fI had this dream\nabout a DROWZEE\veating my dream."
+ .. "\vWhat's this?\vWhere did this TM\vcome from?"
+ .. "\fThis is spooky!\nHere, you can\vhave this TM.",
received = "_ViridianCityFisherReceivedTM42Text",
explain = "_ViridianCityFisherTM42ExplanationText",
noRoom = "_ViridianCityFisherTM42NoRoomText",
diff --git a/data/scripts/story6.lua b/data/scripts/story6.lua
index e28c1230..21de982e 100644
--- a/data/scripts/story6.lua
+++ b/data/scripts/story6.lua
@@ -12,9 +12,13 @@ local function push(game, s, done)
game.stack:push(TextBox.new(game, s, done))
end
+-- PrintText on a text_end string returns with the box still drawn and
+-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
+-- engine/menus/text_box.asm); no A press clears the question first. Ride
+-- TextBox's opts.choice, the same as Commands.ask (#854).
local function ask(game, s, cb)
- local ChoiceBox = require("src.ui.ChoiceBox")
- push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
+ local TextBox = require("src.render.TextBox")
+ game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
end
-- -------------------------------------------------------------------
diff --git a/data/scripts/victories.lua b/data/scripts/victories.lua
index b1aeaa9f..dc71e796 100644
--- a/data/scripts/victories.lua
+++ b/data/scripts/victories.lua
@@ -18,6 +18,16 @@
-- script). Leaders are not def_trainers entries, so engageTrainer has
-- no header.won -- checkVictoryRewards shows this chain instead of a
-- synthetic "received badge/TM" stub.
+--
+-- Gym entries split the TM hand-over out of `dialogue`, mirroring the
+-- originals' GiveItem check (`call GiveItem` / `jr nc, .BagFull`):
+-- `tmPre` is the ReceiveTM script's lead-in (badge info / "Wait! Take
+-- this!"), shown at the victory and again when a beaten leader retries
+-- the hand-over; `tmDialogue` shows only when the TM actually goes in
+-- the bag; `noRoom` is the "make room" line shown instead when the bag
+-- is full; `gotFlag` (pokered's EVENT_GOT_TM*) is set only on a
+-- successful give, which is what makes the leader's talk script retry
+-- later (gyms.lua).
local function range(prefix, first, last)
local t = {}
@@ -33,6 +43,8 @@ return {
-- escort NPC and the first Route 22 rival stay gone after the badge.
["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK",
item = "TM_BIDE",
+ gotFlag = "EVENT_GOT_TM34",
+ noRoom = "_PewterGymTM34NoRoomText",
deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" },
hide = {
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
@@ -41,69 +53,99 @@ return {
dialogue = {
"_PewterGymBrockReceivedBoulderBadgeText",
"_PewterGymBrockBoulderBadgeInfoText",
- "_PewterGymBrockWaitTakeThisText",
+ },
+ tmPre = { "_PewterGymBrockWaitTakeThisText" },
+ tmDialogue = {
"_PewterGymReceivedTM34Text",
"_TM34ExplanationText",
} },
["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY",
item = "TM_BUBBLEBEAM",
+ gotFlag = "EVENT_GOT_TM11",
+ noRoom = "_CeruleanGymMistyTM11NoRoomText",
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
dialogue = {
"_CeruleanGymMistyReceivedCascadeBadgeText",
- "_CeruleanGymMistyCascadeBadgeInfoText",
+ },
+ tmPre = { "_CeruleanGymMistyCascadeBadgeInfoText" },
+ tmDialogue = {
"_CeruleanGymMistyReceivedTM11Text",
} },
["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE",
item = "TM_THUNDERBOLT",
+ gotFlag = "EVENT_GOT_TM24",
+ noRoom = "_VermilionGymLTSurgeTM24NoRoomText",
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
dialogue = {
"_VermilionGymLTSurgeReceivedThunderBadgeText",
- "_VermilionGymLTSurgeThunderBadgeInfoText",
+ },
+ tmPre = { "_VermilionGymLTSurgeThunderBadgeInfoText" },
+ tmDialogue = {
"_VermilionGymLTSurgeReceivedTM24Text",
"_TM24ExplanationText",
} },
["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA",
item = "TM_MEGA_DRAIN",
+ gotFlag = "EVENT_GOT_TM21",
+ noRoom = "_CeladonGymTM21NoRoomText",
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
dialogue = {
"_CeladonGymErikaReceivedRainbowBadgeText",
- "_CeladonGymRainbowBadgeInfoText",
+ },
+ tmPre = { "_CeladonGymRainbowBadgeInfoText" },
+ tmDialogue = {
"_CeladonGymReceivedTM21Text",
"_TM21ExplanationText",
} },
["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA",
item = "TM_TOXIC",
+ gotFlag = "EVENT_GOT_TM06",
+ noRoom = "_FuchsiaGymKogaTM06NoRoomText",
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
dialogue = {
"_FuchsiaGymKogaReceivedSoulBadgeText",
- "_FuchsiaGymKogaSoulBadgeInfoText",
+ },
+ tmPre = { "_FuchsiaGymKogaSoulBadgeInfoText" },
+ tmDialogue = {
"_FuchsiaGymKogaReceivedTM06Text",
"_FuchsiaGymKogaTM06ExplanationText",
} },
["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA",
item = "TM_PSYWAVE",
+ gotFlag = "EVENT_GOT_TM46",
+ noRoom = "_SaffronGymSabrinaTM46NoRoomText",
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
dialogue = {
"_SaffronGymSabrinaReceivedMarshBadgeText",
- "_SaffronGymSabrinaMarshBadgeInfoText",
+ },
+ tmPre = { "_SaffronGymSabrinaMarshBadgeInfoText" },
+ tmDialogue = {
"_SaffronGymSabrinaReceivedTM46Text",
"_TM46ExplanationText",
} },
["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE",
item = "TM_FIRE_BLAST",
+ gotFlag = "EVENT_GOT_TM38",
+ noRoom = "_CinnabarGymBlaineTM38NoRoomText",
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
dialogue = {
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
- "_CinnabarGymBlaineVolcanoBadgeInfoText",
+ },
+ tmPre = { "_CinnabarGymBlaineVolcanoBadgeInfoText" },
+ tmDialogue = {
"_CinnabarGymBlaineReceivedTM38Text",
"_CinnabarGymBlaineTM38ExplanationText",
} },
["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI",
item = "TM_FISSURE",
+ gotFlag = "EVENT_GOT_TM27",
+ noRoom = "_ViridianGymGiovanniTM27NoRoomText",
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
dialogue = {
"_ViridianGymGiovanniReceivedEarthBadgeText",
- "_ViridianGymGiovanniEarthBadgeInfoText",
+ },
+ tmPre = { "_ViridianGymGiovanniEarthBadgeInfoText" },
+ tmDialogue = {
"_ViridianGymGiovanniReceivedTM27Text",
"_ViridianGymGiovanniTM27ExplanationText",
} },
diff --git a/data/scripts/yellow_jessie_james.lua b/data/scripts/yellow_jessie_james.lua
index c1e4f226..7379012e 100644
--- a/data/scripts/yellow_jessie_james.lua
+++ b/data/scripts/yellow_jessie_james.lua
@@ -62,10 +62,15 @@ M.MT_MOON_B2F = {
{ "walk_npc", 6, { "left", "left", "left", "left", "left" } },
{ "face_object", 6, "left" },
{ "show_text", "_MtMoonJessieJamesText2" },
+ -- MtMoonB2FScript12 arms _MtMoonJessieJamesText3 with
+ -- SaveEndBattleTextPointers before it sets wCurOpponent, so
+ -- TrainerBattleVictory prints it on the battle screen as "ROCKET: A
+ -- brat beat us?" between TrainerDefeatedText and MoneyForWinningText.
+ -- Its one-word first line only reads right behind that tag (#866).
+ { "save_end_battle_text", "_MtMoonJessieJamesText3" },
{ "start_battle", "trainer", "OPP_ROCKET", 42 },
{ "check_battle_result", "win" },
{ "jump_if_false", "end" },
- { "show_text", "_MtMoonJessieJamesText3" },
{ "show_text", "_MtMoonJessieJamesText4" },
{ "stop_music" },
{ "play_music", "Music_MeetJessieJames" },
@@ -85,7 +90,8 @@ M.MT_MOON_B2F = {
-- motto plays from off-screen FIRST, then the duo pops in at (25,10) /
-- (24,10) and whichever of them shares the player's column ($18=24 or
-- $19=25, EVENT_ROCKET_HIDEOUT_4_JESSIE_JAMES_ON_LEFT) walks the three
--- tiles down to loom over the player while the other steps one. A loss
+-- tiles down to loom over the player while the other walks four and ends
+-- up beside him. A loss
-- re-hides them (RocketHideoutB4FResetScripts via EVENT_6A0), so the
-- trigger re-arms clean.
-- -------------------------------------------------------------------
@@ -106,7 +112,7 @@ M.ROCKET_HIDEOUT_B4F = {
if f.EVENT_BEAT_ROCKET_HIDEOUT_4_JESSIE_JAMES then return false end
-- ON_LEFT: player under James's column (25); movement data pairs
-- RocketHideoutB4FJessieJamesMovementData_45605/45606 swap so the
- -- column-mate walks 3, the other 1.
+ -- column-mate walks 3, the other 4.
local onLeft = (x == 25)
ow.runner:run({
{ "stop_music" },
@@ -116,16 +122,30 @@ M.ROCKET_HIDEOUT_B4F = {
{ "emote", "player", "shock", 30 },
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JAMES" },
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JESSIE" },
- -- James (object 2) then Jessie (object 3), Script4..Script9 order
- { "walk_npc", 2, onLeft and { "down", "down", "down" } or { "down" } },
+ -- James (object 2) then Jessie (object 3), Script4..Script9 order.
+ -- RocketHideoutB4FJessieJamesMovementData_45605 is a lone $4 that FALLS
+ -- THROUGH into _45606 ($4 $4 $4 $ff), so MoveSprite_ (home/pathfinding.asm)
+ -- reads _45605 as FOUR steps and _45606 as three; $4 is DOWN in Yellow's
+ -- Func_5288 lookup (engine/overworld/movement.asm), which walks with no
+ -- collision test. From (25,10)/(24,10) against a player on y=14 the
+ -- column-mate stops three down, right above him, and the other walks the
+ -- full four to stand alongside -- which is what the facings below assume.
+ -- Reading _45605 as a single step stranded whoever was off-column three
+ -- tiles away, so James never reached the player (#865).
+ { "walk_npc", 2, onLeft and { "down", "down", "down" }
+ or { "down", "down", "down", "down" } },
{ "face_object", 2, onLeft and "down" or "left" },
- { "walk_npc", 3, onLeft and { "down" } or { "down", "down", "down" } },
+ { "walk_npc", 3, onLeft and { "down", "down", "down", "down" }
+ or { "down", "down", "down" } },
{ "face_object", 3, onLeft and "right" or "down" },
{ "show_text", "_RocketHideoutJessieJamesText2" },
+ -- RocketHideoutB4FScript10 saves _RocketHideoutJessieJamesText3 as the
+ -- end-battle text, so it prints as "ROCKET: Such a dreadful twerp!" on
+ -- the battle screen ahead of MoneyForWinningText (#866).
+ { "save_end_battle_text", "_RocketHideoutJessieJamesText3" },
{ "start_battle", "trainer", "OPP_ROCKET", 43 },
{ "check_battle_result", "win" },
{ "jump_if_false", "lost" },
- { "show_text", "_RocketHideoutJessieJamesText3" },
{ "show_text", "_RocketHideoutJessieJamesText4" },
{ "stop_music" },
{ "play_music", "Music_MeetJessieJames" },
@@ -175,16 +195,27 @@ M.POKEMON_TOWER_7F = {
{ "show_text", "_PokemonTowerJessieJamesText1" },
{ "face_player_dir", "up" },
{ "emote", "player", "shock", 30 },
- -- Jessie (object 1) then James (object 2), Script1..Script6 order
- { "walk_npc", 1, onLeft and { "down" } or { "down", "down", "down" } },
+ -- Jessie (object 1) then James (object 2), Script1..Script6 order.
+ -- Same fall-through blob as the hideout: PokemonTower7FMovementData_60d7a
+ -- is a lone $4 running into _60d7b ($4 $4 $4 $FF), so _60d7a is FOUR
+ -- steps and _60d7b is three. From (10,8)/(11,8) against a player on
+ -- y=12 the column-mate halts one tile above him and the other closes the
+ -- full four to his side; the single-step reading is why James only
+ -- "moved a bit" here (#865).
+ { "walk_npc", 1, onLeft and { "down", "down", "down", "down" }
+ or { "down", "down", "down" } },
{ "face_object", 1, onLeft and "right" or "down" },
- { "walk_npc", 2, onLeft and { "down", "down", "down" } or { "down" } },
+ { "walk_npc", 2, onLeft and { "down", "down", "down" }
+ or { "down", "down", "down", "down" } },
{ "face_object", 2, onLeft and "down" or "left" },
{ "show_text", "_PokemonTowerJessieJamesText2" },
+ -- PokemonTower7FScript7 saves _PokemonTowerJessieJamesText3 as the
+ -- end-battle text: "ROCKET: You will regret this!" on the battle screen,
+ -- before the prize money (#866).
+ { "save_end_battle_text", "_PokemonTowerJessieJamesText3" },
{ "start_battle", "trainer", "OPP_ROCKET", 44 },
{ "check_battle_result", "win" },
{ "jump_if_false", "end" },
- { "show_text", "_PokemonTowerJessieJamesText3" },
{ "show_text", "_PokemonTowerJessieJamesText4" },
{ "stop_music" },
{ "play_music", "Music_MeetJessieJames" },
@@ -254,10 +285,12 @@ M.SILPH_CO_11F = {
{ "walk_npc", 6, jessieDirs },
{ "face_object", 6, jessieFace },
{ "show_text", "_SilphCoJessieJamesText2" },
+ -- SilphCo11FScript11 saves _SilphCoJessieJamesText3 (SilphCo11FText_624c2)
+ -- as the end-battle text: "ROCKET: Like always..." before the money (#866).
+ { "save_end_battle_text", "_SilphCoJessieJamesText3" },
{ "start_battle", "trainer", "OPP_ROCKET", 45 },
{ "check_battle_result", "win" },
{ "jump_if_false", "end" },
- { "show_text", "_SilphCoJessieJamesText3" },
{ "show_text", "_SilphCoJessieJamesText4" },
{ "stop_music" },
{ "play_music", "Music_MeetJessieJames" },
diff --git a/docs/architecture.md b/docs/architecture.md
index a1d58df6..09bbc60d 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -38,7 +38,7 @@ the same core data and graphics into the source tree for verification.
| | `src/core/SaveData.lua` | Lua-serialized save in the LÖVE save dir |
| render | `src/render/Renderer.lua` | 160x144 canvas, integer nearest scaling |
| | `src/render/TileRenderer.lua` | one SpriteBatch per map (8x8 quads) + border-block ring |
-| | `src/render/SpriteRenderer.lua` | 6-frame walker sheets, flipped right facing |
+| | `src/render/SpriteRenderer.lua` | variable-size anchored sprite sheets, 6-frame walkers and flipped right facing |
| | `src/render/Font.lua` | glyph rendering via charmap (greedy longest match) |
| | `src/render/TextBox.lua` | dialogue box: typewriter, `\n` line, `\v` scroll, `\f` page |
| | `src/render/Camera.lua`, `Transition.lua` | follow camera, warp fades |
diff --git a/docs/behavior-porting-notes.md b/docs/behavior-porting-notes.md
index 8693bcef..d26affce 100644
--- a/docs/behavior-porting-notes.md
+++ b/docs/behavior-porting-notes.md
@@ -234,8 +234,12 @@ What was ported from pokered's engine code and where it came from.
pre-battle text and engages the leader battle (badge/TM via
data/scripts/victories.lua); post-badge talk prints the leader's
post-battle advice text (Misty's is her TM11 explanation). The
- originals' middle branch (beaten but TM not handed over) is
- unreachable since the TM is granted with the victory. Giovanni's
+ originals' middle branch (beaten but TM not handed over,
+ CheckEventReuseA EVENT_GOT_TM*) is ported too: the victory's GiveItem
+ goes through the bag's capacity check, a full bag shows the leader's
+ "make room" text instead of the received lines and leaves
+ EVENT_GOT_TM* unset, and talking to the leader re-runs the ReceiveTM
+ script until the TM goes in (#797). Giovanni's
farewell (`ViridianGymGiovanniText` .afterBeat) hides him inside a
fade-to-black/fade-in Transition matching ViridianGym.asm's
GBFadeOutToBlack → HideObject → GBFadeInFromBlack, persisted
diff --git a/docs/launcher.md b/docs/launcher.md
index 065d40d0..c9a43616 100644
--- a/docs/launcher.md
+++ b/docs/launcher.md
@@ -195,11 +195,13 @@ through `src/import/SaveFileIO.lua`, which sits on top of
(checked against `listSlots`). `SaveFileIO.exportActiveSlot` loads the active
slot, encodes it back with `SaveConvert.exportSav` (a slot never keeps
`rawImport`, so this is a zero-filled template export, which is valid), and
- writes `exports//gen1recomp--.sav` in the save
- directory (`exports/` and `exports//` are created as needed). On
- desktop it returns the absolute path (`love.filesystem.getSaveDirectory()`),
- which the notice line shows with an "Open folder" affordance
- (`love.system.openURL("file://" .. dir)`).
+ writes `exports//gen1recomp--.sav` under the same
+ root `persistFs` writes slots to: the portable game folder when `portable.txt`
+ marks the install, otherwise the save directory (`exports/` and
+ `exports//` are created as needed; #752). On desktop it returns the
+ absolute path (`SaveData.portableBaseDir()` when portable, else
+ `love.filesystem.getSaveDirectory()`), which the notice line shows with an
+ "Open folder" affordance (`love.system.openURL("file://" .. dir)`).
On Android the bytes are also staged as `pending_export.sav` and
`love.system.createFile(suggestedName)` opens `ACTION_CREATE_DOCUMENT` so the
player can save to Downloads / Drive / etc.; on return `export_done.flag`
diff --git a/docs/linux-arm64-build.md b/docs/linux-arm64-build.md
new file mode 100644
index 00000000..b02e0ced
--- /dev/null
+++ b/docs/linux-arm64-build.md
@@ -0,0 +1,202 @@
+# Linux arm64 (aarch64) AppImage
+
+Releases ship `gen1recomp--linux-arm64.AppImage` alongside the
+existing x86_64 `gen1recomp--linux.zip`. It targets 64-bit ARM
+desktop Linux: Raspberry Pi 4/5 running Raspberry Pi OS, Armbian and other
+SBC distros, arm64 VMs on Apple Silicon, Ampere/Graviton desktops, and the
+aarch64 handhelds that run a full distro.
+
+> The Anbernic RG34XXSP has its own PortMaster-style pack
+> (`gen1recomp-*-rg34xxsp-stockos64-mod.zip`, see
+> [anbernic-rg34xxsp.md](anbernic-rg34xxsp.md)). That one bundles PortMaster's
+> LÖVE runtime and expects the device's own SDL; this AppImage is the generic
+> desktop-Linux artifact and shares nothing with it but the `game.love`.
+
+## For players
+
+```sh
+chmod +x gen1recomp-*-linux-arm64.AppImage
+./gen1recomp-*-linux-arm64.AppImage
+```
+
+Then use **Import ROM** in the launcher to point it at your own legal Red /
+Blue / Yellow cartridge dump, exactly as on every other platform.
+
+If your system has no FUSE (`dlopen(): error loading libfuse.so.2`), either
+install it (`sudo apt install libfuse2`) or run without it:
+
+```sh
+./gen1recomp-*-linux-arm64.AppImage --appimage-extract-and-run
+```
+
+### What the host has to provide
+
+Very little, and this is enforced by an assertion in the build rather than by
+good intentions. The only libraries the AppImage requires at startup are:
+
+```
+glibc 2.29+ libstdc++ libfreetype6 zlib
+```
+
+Everything else — OpenGL/Mesa, X11, Wayland, KMSDRM, ALSA, PulseAudio — is
+**dlopened**, so it is used when present and skipped when absent. That means
+one image runs on a full desktop, on a Wayland-only session, on a
+KMSDRM-only handheld with no X server, and on a box with ALSA but no
+PulseAudio, without a different build for each.
+
+That property does not come for free from Debian's packages, and getting it
+is most of what the build below is doing; see
+[Why five libraries are built from source](#why-five-libraries-are-built-from-source).
+
+## For builders
+
+```sh
+scripts/build_linux_arm64.sh --version 0.1.0
+```
+
+Output:
+
+```
+dist/linux-arm64/gen1recomp--linux-arm64.AppImage
+dist/linux-arm64/gen1recomp--linux-arm64.AppImage.sha256
+```
+
+Useful flags: `--game-love PATH` reuses an already-packed payload (CI does
+this so every platform ships identical bytes), `--rebuild-image` forces the
+builder container to rebuild, `--clean-cache` throws away the pinned
+downloads and the compiled LÖVE prefix.
+
+### Requirements
+
+An **aarch64 host** with **docker or podman**. A Raspberry Pi 5 is the
+reference machine (a cold build takes about 10 minutes on one — six libraries
+plus the engine; rebuilds reuse the cached prefix and take seconds). Apple Silicon with Docker
+Desktop and GitHub's `ubuntu-24.04-arm` runner both work too.
+
+The script refuses to run on x86_64 rather than falling back to qemu-user
+emulation: that path takes hours and has produced miscompiled LuaJIT.
+
+### Why this is not just another `scripts/build.sh` target
+
+`scripts/build.sh linux` downloads LÖVE's official `love-11.5-x86_64.AppImage`,
+unpacks its squashfs, drops `game.love` in, and glues it back together. That
+trick is not available here — **LÖVE publishes no aarch64 binary at all.** The
+11.5 release has win32, win64, macOS, Android, iOS and one x86_64 AppImage,
+and that is the entire list.
+
+So this build compiles LÖVE 11.5 from the official `linux-src` tarball and
+assembles the AppImage from scratch. Every pinned input — the LÖVE source, the
+five libraries built alongside it, and the AppImage type-2 runtime — is
+SHA-256 verified on the host before the container ever sees it, and the
+container itself runs with no network access.
+
+### Why the build happens in a Debian bullseye container
+
+glibc is backward compatible but not forward compatible: a binary linked
+against glibc 2.41 will not start on a system with 2.31, and there is no way
+to fix that after the fact. Compiling on the oldest base we support is
+therefore the only thing that makes one artifact work everywhere.
+
+Bullseye (glibc 2.31) is that base. The resulting binaries actually come out
+needing only **glibc 2.29** and **GLIBCXX_3.4.21**, so the AppImage covers
+everything from Ubuntu 20.04 and Raspberry Pi OS bullseye through current
+trixie.
+
+This is a statement about the *compile environment*, not about where the
+artifact runs — building on your own newer distro would silently raise that
+floor and strand every user on an older one, with no symptom until they
+download it. CI enforces the floor: `linux-arm64-build` fails if the highest
+required glibc symbol version climbs above 2.31.
+
+### Why five libraries are built from source
+
+SDL2, OpenAL, libtheora, libogg/libvorbis and libmpg123 are compiled rather
+than installed from bullseye. In every case the reason is *correctness*, not
+a newer version number — Debian builds these for a system where every
+dependency is installed and co-versioned, which is the opposite of an
+AppImage's situation. Each one broke the build in a different way, and all
+three failure modes are now assertions that fail the build instead of
+shipping.
+
+**1. Hard-linked backends (SDL2, OpenAL).** Debian's `libSDL2` lists
+`libpulse`, `libasound`, `libX11` and `libwayland-client` as `DT_NEEDED` —
+resolved by the loader at startup, not dlopened. An AppImage bundling it
+refuses to start unless the host has *all four*. It appeared to work in
+testing only because a desktop Pi has all four; a headless CI runner is what
+exposed it. Debian's OpenAL does the same via `libsndio`, which itself
+hard-links `libasound`. Built from source with `--enable-*-shared` and
+`ALSOFT_DLOPEN`, both dlopen their backends instead.
+
+**2. A stray link (libtheora).** Debian's `libtheoradec.so.1` is linked
+against `libcairo.so.2` — a packaging artifact, since a video decoder has no
+business drawing vector graphics — and cairo drags in X11, xcb, fontconfig
+and freetype. `--disable-examples` produces a `libtheoradec` needing only
+`libogg`.
+
+**3. SONAME collision with the host (ogg, vorbis, mpg123).** The subtle one.
+OpenAL dlopens ALSA, ALSA's config loads its PulseAudio hook plugin, and that
+plugin pulls the *host's* `libsndfile` into our process. `libsndfile` links
+`libogg`, `libvorbis` and `libmpg123` — the same three we bundle. The loader
+resolves a SONAME exactly once per process, so the host's `libsndfile` binds
+to *our* copies:
+
+```
+openal -> libasound -> libasound_module_conf_pulse -> libsndfile (host, new)
+ `-> mpg123_info2 -> libmpg123 (ours, bullseye 1.26)
+```
+
+`mpg123_info2` arrived in mpg123 1.32, so the plugin failed to relocate, ALSA
+config collapsed, and the game ran with **no audio device at all**. Not
+bundling these instead would make `libogg`/`libvorbis`/`libmpg123` mandatory
+host packages; building them current means our copies *satisfy* the host's
+`libsndfile` rather than starving it.
+
+The same collision is why the font stack — freetype, fontconfig, libpng,
+brotli, zlib — is left to the host entirely. Bundling a bullseye freetype
+2.10.4 meant a host `libcairo` could not find `FT_Get_Transform` (added in
+2.11) and the game died at startup. Leaving the whole stack to the host keeps
+it self-consistent, while `liblove` — compiled against 2.10.4 — only ever
+asks for symbols every supported host already has.
+
+The general rule this all reduces to: **never bundle a library the host's own
+stack may also load, unless yours is at least as new as theirs.**
+
+### CI
+
+Three jobs, path-gated on `scripts/build_linux_arm64.sh`,
+`scripts/linux-arm64/`, `scripts/pack_love.sh` and this document:
+
+- **`linux-arm64-selftest`** (`ubuntu-latest`, x86_64) — offline gate. Checks
+ the pins are real digests on a dated tag rather than the moving
+ `continuous` one, that the Dockerfile still builds on bullseye, that the
+ exclude list still classifies known sonames correctly, that AppRun still
+ launches `game.love` with `--fused`, and that the host-arch guard actually
+ fires. Needs no container and no arm64 machine.
+- **`linux-arm64-build`** (`ubuntu-24.04-arm`) — the real build, then extracts
+ the artifact and asserts the layout, that every bundled object resolves
+ under AppRun's `LD_LIBRARY_PATH`, and that the glibc floor is still ≤ 2.31.
+ Uploads the AppImage for 7 days.
+- **release** — `linux-arm64` runs on `ubuntu-24.04-arm`, reuses the shared
+ `game.love` from the `love-payload` job, and the AppImage is staged and
+ published like every other release asset.
+
+Unlike the Switch job, none of this needs secrets or self-hosted hardware, so
+it runs on fork PRs too.
+
+### Updating the pins
+
+Both pins live in `scripts/linux-arm64/common.sh`:
+
+- `LOVE_VERSION` / `LOVE_SRC_SHA256` — bumping any version invalidates the
+ cached prefix automatically (its name is keyed by every source version at
+ once, so a partial rebuild cannot mix vintages). Check that bullseye still
+ has `-dev` packages new enough for the new release; `build_appimage.sh`
+ asserts every optional module actually linked, because LÖVE's `configure`
+ exits 0 and silently drops a module when one is missing.
+- `SDL2_*`, `OPENAL_*`, `THEORA_*`, `OGG_*`, `VORBIS_*`, `MPG123_*` — the
+ source-built libraries. Bumping these is usually safe and occasionally
+ necessary: `libmpg123` in particular must stay at least as new as what a
+ target host's `libsndfile` expects, which is asserted for `mpg123_info2`.
+- `APPIMAGE_RUNTIME_TAG` / `APPIMAGE_RUNTIME_SHA256` — always a dated tag
+ from [AppImage/type2-runtime](https://github.com/AppImage/type2-runtime/releases).
+ The selftest fails the build if this ever points at `continuous`.
diff --git a/docs/modding.md b/docs/modding.md
index 5aa0bec0..b13fb048 100644
--- a/docs/modding.md
+++ b/docs/modding.md
@@ -35,6 +35,16 @@ An edited vanilla map becomes a `mod.content.maps:patch` carrying only the
fields that moved; a new map becomes a `:register`. See
`docs/new-features.md` and the extension's own README.
+## Read-only map overviews
+
+`mod.world:mapOverview()` returns collision `rows` at map-cell resolution,
+optional visual `tileRows` at 2x resolution, and optional `tileDetailRows` at
+4x resolution. Visual rows contain Game Boy shades from `"0"` (lightest) to
+`"3"` (darkest); their matching width and height fields describe the grid.
+`markers` contains active `{ kind, x, y }` points in map-cell coordinates for
+`warp`, visible `item`, and untaken `hidden` locations. All fields are
+read-only snapshots; mods choose which layers to render.
+
## Rendering pipelines
Most registries hand the engine *content*. `render_pipelines` hands it
@@ -100,6 +110,44 @@ Three rules worth knowing:
Returning `nil` from `drawWorld` is a normal answer meaning "not this
frame"; the engine draws the vanilla world instead.
+## Variable-size overworld sprites
+
+The `sprites` registry keeps the vanilla 16x16 grounded walker as its default,
+but a mod can describe any frame rectangle and anchor for player characters,
+NPCs, followers, mounts, vehicles, bosses, or other field actors:
+
+```lua
+mod.content.sprites:register("SPRITE_COMPANION", {
+ image = "mods/example/companion.png", -- one frame per row
+ frames = 6,
+ walker = true,
+ frameWidth = 32,
+ frameHeight = 32,
+ anchorX = 16, -- frame-relative bottom-center anchor
+ anchorY = 32,
+})
+```
+
+`frameWidth` and `frameHeight` are sheet pixels. `anchorX` and `anchorY` are
+measured from each frame's top-left; when omitted they default to the frame's
+horizontal center and bottom edge, so a larger sprite grows upward while its
+feet stay on the same world cell. Omitting all four fields is exactly the
+vanilla 16x16 placement. The normal player/NPC/follower draw paths consume
+these values automatically, including horizontal flips and the fishing pose.
+
+Custom render pipelines can use the same geometry without reproducing the
+pose rules:
+
+```lua
+local geometry = sprite:getPoseGeometry(facing, walkPhase, stepFlip)
+-- geometry.quad, .x/.y/.width/.height, .anchorX/.anchorY, .mirror
+local originX, originY = sprite:getScreenOrigin(px, py, camX, camY)
+```
+
+`getFrameGeometry(frame)` is the corresponding accessor for a specific
+zero-based sheet frame. Both accessors return fresh tables and share the
+renderer’s frame selection and mirror conventions.
+
## Battle sprite scaling
The enemy's front pic draws at 1x and the player's back pic at 2x, the way
@@ -140,6 +188,90 @@ default** (1x front, 2x back).
ball-to-pic grow multiplies your scale through each stage, so a rescaled
mon still grows into place from the ball, grounded the whole way.
+## Durable tool storage and runtime checkpoints
+
+`mod.save` remains the right place for state that should travel with the next
+normal Pokémon SAVE. Tools that need independently written, larger data-only
+records can use `mod.storage`; the engine scopes every logical key by game
+version, opaque playthrough identity, and mod id, and routes it through the same
+standard or portable persistence backend as saves:
+
+```lua
+local context, code, message = mod.storage:context(game)
+local ok, code, message = mod.storage:write(game, "history/quick/q0001", {
+ format = 1, createdAt = os.time(), payload = { money = 3000 },
+})
+local value, code, message = mod.storage:read(game, "history/quick/q0001")
+local keys, code, message = mod.storage:list(game, "history/quick")
+local deleted, code, message = mod.storage:delete(game, "history/quick/q0001")
+```
+
+`context` returns `{ engineVersion, gameVersion, playthroughId }`. The engine
+version is compatibility metadata; physical launcher-slot and path identity stays
+private.
+
+Values must be tables containing serializable data only. Keys are conservative
+slash-separated segments (letters, digits, `_`, `-`); paths and filesystem
+handles are never exposed. Writes are staged and decode-verified, reads recover
+from a valid staged/backup generation, and methods return structured errors for
+normal data or I/O failures. The playthrough identity is allocated lazily on the
+first storage/checkpoint call, so an unused API changes no save bytes.
+
+`mod.checkpoints` captures and reconstructs engine-owned semantic runtime state:
+
+```lua
+local capability = mod.checkpoints:inspect(game)
+if capability.canCapture then
+ local checkpoint, code, message = mod.checkpoints:capture(game)
+ -- Store the detached data-only checkpoint through mod.storage.
+end
+
+local ok, code, message = mod.checkpoints:restore(game, checkpoint)
+```
+
+Checkpoint format 1 supports settled overworld control and proven battle
+player-decision safe points. Battle checkpoints are limited to ordinary
+single-player wild/trainer origins with no suspended script; link, Safari,
+ghost, demo, scripted, animation, message, queue, and forced-action phases fail
+closed. New checkpoints preserve gameplay RNG, while legacy overworld records
+without RNG remain loadable. Capture excludes global options and runtime
+objects. Restore validates format, game/playthrough identity, content,
+coordinates, battle relationships, continuation, and RNG before mutation;
+preserves current options; suppresses normal map-entry/save-load/intro side
+effects; verifies a recapture; and rolls back runtime plus RNG in memory if
+reconstruction fails. Callers that need crash recovery should durably capture
+their own recovery checkpoint before restore.
+
+Checkpoint ownership follows the persistence model rather than mod identity:
+
+- canonical `game.save` progress, including every mod's `save.modData` /
+ `mod.save` bucket and data-only fields added to saved Pokémon, rewinds;
+- global and per-mod options remain at their current values;
+- independently written `mod.storage` records do not rewind; and
+- mod-owned runtime objects, references, and caches are never serialized.
+
+Successful restore emits `checkpoint.restored` only after reconstruction and
+differential recapture have committed. Mods that cache rewound progress or hold
+references to reconstructed runtime objects can re-read their own public state
+and rebuild at that point:
+
+```lua
+mod.events:on("checkpoint.restored", function(ev)
+ -- ev.kind is "overworld" or "battle"; ev.game is fully reconstructed.
+ cachedQuestStage = mod.save:get("quest_stage", 0)
+ rebuildRuntimeFor(ev.game, ev.kind)
+end)
+```
+
+The event is not emitted for validation failure, failed reconstruction, or a
+successful rollback. Its payload contains no checkpoint data or other mod's
+private state. A mod that deliberately stores progress-coupled truth in
+`mod.storage` must version and reconcile that relationship itself; the engine
+cannot distinguish it safely from independent history, configuration, or cache
+data.
+
+See RFC 0003, RFC 0004, and RFC 0005 for exact contracts and error codes.
+
## Developer console
Boot with developer mode on to unlock the in-game console and hot-reload
@@ -174,6 +306,32 @@ It runs immediately before queued button edges are promoted, so input added by
the wrapper is visible during that same fixed step. The callback receives
`(next, game, dt)` and must call `next(game, dt)`.
+`input.pointer` delivers uncaptured gameplay pointer events -- touches and
+real mouse input alike. The callback receives `(next, game, ev)` where `ev`
+is `{ phase, source, id, x, y, dx, dy, pressure, button }`: `phase` is
+`"pressed"`, `"moved"`, `"released"` or `"cancelled"`; `source` is `"touch"`
+or `"mouse"`; `id` is the LÖVE touch id or `"mouse"`; and the coordinates
+are LOVE window units, the same space `render.hud`'s viewport and the touch
+overlay lay out in. The on-screen touch controls keep first refusal: a
+pointer that begins on a virtual control belongs to the pad for its whole
+lifecycle and never reaches the hook, while one that begins outside stays
+visible even if it later crosses a control. A real mouse reaches the hook
+without `POKEPORT_TOUCH` (synthesized `istouch` mouse twins are dropped, so
+a mobile touch fires once), and focus or visibility loss and input recovery
+deliver a `"cancelled"` for every pointer the hook saw pressed but not yet
+released. Return `true` without calling `next` to consume the event.
+
+`mod.input` presses GB buttons source-safely. `mod.input:tap(game, btn)`
+queues exactly one `wasPressed` edge for the next fixed step and holds
+nothing; `local token = mod.input:press(game, btn)` holds the button until
+`mod.input:release(token)`. Buttons are `up`, `down`, `left`, `right`, `a`,
+`b`, `start` and `select`. Every press is its own input source inside the
+engine's multi-source bookkeeping, so releasing a token never clears a hold
+the keyboard, a controller, the touch overlay or another mod still owns;
+`release` is idempotent and refuses tokens taken by another mod.
+Outstanding tokens are released automatically on entry-chunk rollback, hot
+reload and input recovery.
+
`ui.title_menu.items` receives `(next, game, items)` and follows the same
decorate-after-`next` convention as `ui.start_menu.items`. It is the safe place
for a tool to offer a fresh-session action before gameplay begins.
@@ -195,10 +353,76 @@ the finished `worldCanvas` and `uiCanvas` with their SGB `zones` / `worldZones`,
`worldActive`, the frame metrics (`ww`, `wh`, `pw`, `ph`, `ox`, `oy`, `vpw`,
`vph`, `scale`, `Sx`, `Sy`, `dpiX`, `dpiY`), `renderer:blitCanvas(...)` for a
palette-correct blit of either canvas into an arbitrary screen rect, and the
-`secondScreen` bridge (`available()` / `push(imageData, w, h)` / `setEnabled`)
-for driving a second physical display. This is what lets a mod lay the two
-passes out as two stacked Game Boy screens, or push one onto a second screen,
-without the engine knowing the layout.
+`secondScreen` bridge (`available()` / `push(imageData, w, h)` / `pollTouch()` /
+`setEnabled`) for driving a second physical display. `pollTouch()` returns the
+oldest queued event as `"action,x,y"` in submitted-frame coordinates, or `nil`.
+This is what lets a mod lay the two passes out as two stacked Game Boy screens,
+or push one onto a second screen, without the engine knowing the layout.
+
+`screen.render_visible` receives `(next, state)` while the main screen is being
+composed. Return `false` to omit that state from drawing, opacity selection and
+palette-zone ownership. The state remains on the stack and keeps its normal
+update and input ownership, so a mod can mirror a native menu on another
+display without reimplementing it. The default is `true`. Treat the wrapper as
+a pure predicate: the renderer may ask it more than once per frame.
+
+Scrollable list states expose `state.kind` for use with this hook. Generic
+lists fall back to their title; PC lists use stable, localization-independent
+identifiers: `pc_box_withdraw`, `pc_box_deposit`, `pc_box_release`,
+`pc_box_change`, `pc_item_withdraw`, `pc_item_deposit`, and `pc_item_toss`.
+
+`battle.bottom_ui_visible` and `battle.status_hud_visible` independently
+control the battle text/menu layer and the HP/status panels. Both receive
+`(next, state)` and default to `true`, so vanilla rendering is unchanged.
+Pushed text boxes also pass through `battle.bottom_ui_visible`; a wrapper that
+only owns battle presentation should return `false` only for its active battle
+or text-box state.
+
+`core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call
+(once per frame). Vanilla behavior resolves the per-category GAME SPEED
+option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever
+category `Game.speedCategoryInStack` says is active right now. A mod may
+call `next(game)` and return its result to pass that resolution through, or
+return a different number outright to override it for that frame (a bot mod
+forcing 1X for one route segment, say, regardless of the category or saved
+option). The result is clamped to the nearest valid `GameSpeed.LEVELS` entry
+regardless of what a subscriber returns, so a bad value (0, negative, `nil`)
+cannot destabilize the fixed-step accumulator. This hook runs *after* link
+play's 1X lock and the `--speed`/equivalent run-argument override, both of
+which stay unconditional and are never visible to a subscriber.
Developer mode also arms the mod loader's dev tripwire, which flags mods
that reach outside their permission set.
+
+## Process-lifecycle hooks
+
+These exist so a platform-specific launcher integration (a native shell
+that embeds this engine and wraps its window in platform UI) can live
+entirely in a mod instead of hand-patching `main.lua`, which every other
+engine change also touches.
+
+`core.update` receives `(next, game, dt)` once per frame from
+`love.update`. Vanilla behavior is `game:update(dt)`, unconditionally. A
+mod may skip calling `next(game, dt)` to pause the simulation for that
+frame (e.g. while a native settings sheet is on top), and may run
+additional per-frame polling before or after that call regardless of
+whether it calls `next` -- useful for one-shot flags that must be observed
+every frame even while paused.
+
+`core.quit_to_launcher` receives `(next)` once from `love.quit()`. `next()`
+returns the engine's own decision for whether closing the window should
+return to the Lua launcher instead of exiting; a mod may return `false`
+outright, without ever calling `next`, to veto that and let the process
+really quit -- for a platform host that owns its own "return to launcher"
+UI and would otherwise get looped straight back into the game it just
+quit.
+
+A manifest may also declare `force_enable_env`, an environment variable
+name that re-enables the mod regardless of a saved disable in
+`options.mods` when that variable is set to `"1"`. This is for a mod that
+cannot function disabled on the one build where its env var is set (a
+platform-bridge mod bundled only with that build's launcher, for example).
+
+Neither hook needs a `Runtime.wantsHook` guard before calling it: `Hooks:call`
+already falls straight through to the vanilla function when no mod has
+wrapped the name, at negligible cost.
diff --git a/docs/new-features.md b/docs/new-features.md
index ea28584b..93c6d23e 100644
--- a/docs/new-features.md
+++ b/docs/new-features.md
@@ -1,531 +1,26 @@
-# New features (deliberate additions beyond the original)
-
-Intentional enhancements this port adds on top of faithful Pokémon Red
-behavior. They have no Game Boy equivalent and are kept by design.
-Genuine divergences from the original (things still missing, wrong, or
-approximated) live in docs/known-differences.md; faithfully-ported
-behavior is in docs/behavior-porting-notes.md.
-
-## Survey zoom
-
-The mouse wheel (or `-`/`=`), the Options **ZOOM** row, or hotkey `4`
-zooms the overworld between 1 pixel per world pixel (full survey) and 2×
-the window fit scale (close-up), in crisp integer steps. This has no Game
-Boy equivalent:
-
-- Connected maps render their full bodies, and their NPCs appear as
- visual-only "ghosts", they wander but have no sight lines, triggers,
- dialogue, or collision until the map is actually entered.
-- Menus, text boxes, and battles draw at normal scale on top of the
- zoomed world. Zoom input is ignored while a script, menu, or battle is
- active; the zoom offset is persisted as `save.options.zoom` (default
- `0` = FIT) and survives New Game via `options.lua`.
-- Hotkey `4` ticks through every integer zoom level (survey → FIT →
- close-up → wrap). The Options row shows `FIT` / `OUTn` / `INn`.
-- Beyond the border ring the void fill repeats indefinitely (see VOID
- FILL below); interiors keep their own border block. Each visible map
- area is colorized with its own SGB palette (the original recolored the
- whole screen per map).
-- Neighbor maps load two connection hops out so corner-adjacent maps
- don't pop in and out, and ghost NPCs share instances with the real ones
- so their wander positions persist across seamless connection crossings
- (a warp or fresh map entry still respawns everything at its script
- position, like the original's per-entry sprite init).
-
-## VOID FILL
-
-The Options **VOID FILL** row picks what paints the infinite beyond-edge
-space on OVERWORLD-tileset maps during survey zoom:
-
-- **TREES** (default): solid tree wall block `$0F`.
-- **WATER**: animated water tile `$14` (same hshift cycle as on-map water).
-- **BLACK**: solid black.
-
-Other tilesets are unchanged (house/cave borders stay as authored).
-Persisted as `save.options.voidFill`.
-
-## Tilt mode
-
-The `3` key (and the Options menu TILT row) cycles a visual-only perspective
-tilt of the overworld through **OFF → 15° → 35° → 50° → OFF** for an HD-2D /
-diorama look. Like survey zoom this is purely presentational and has no
-Game Boy equivalent:
-
-- The entire map tilts as one rigid ground plane, paths, grass, water,
- floors, and every background-tile structure (buildings, trees, fences,
- signs; in Gen 1 these are baked into the tile layer, not sprites), so
- rows above the player recede and rows below come toward the viewer. Only
- things that actually *stand* on the ground draw as upright billboards,
- unscaled and pixel-identical to flat mode: the player, NPCs, item balls,
- and the standing FX attached to them (emote bubbles, the fishing rod,
- the FLY bird). The Poké Center heal-machine overlay stays on the ground
- plane with the machine tiles (it is OAM glued to a BG graphic, not a
- standing sprite). An earlier revision tried
- billboarding buildings/trees/signs too (cutting them out of the ground
- per hand-curated per-tileset tables); that chased an endless tail of
- special cases, dense tree canopy, fences fused into grass, building
- facades with their own baked-in fake perspective, because Gen 1's art
- was never drawn with a clean seam between ground and standing scenery. It
- wasn't merged; tilting everything but the characters as one plane is the
- simpler, shipped tradeoff (buildings recede/foreshorten with the ground
- like a photo of a diorama, rather than standing fully upright next to
- a full-height character).
-- Cycling tweens the angle between levels over ~0.25s rather than snapping;
- with tilt fully off the world pass drops back onto the flat blit path, so
- flat rendering stays pixel-identical to tilt-off and off costs nothing.
-- Tilt input is gated exactly like survey zoom, honored only while
- free-roaming, ignored while a script, menu, or battle is active, and it
- composes with survey zoom (the zoom scale feeds the projection). The tilt
- level is persisted in `save.options.tilt` (default OFF).
-- It applies everywhere the overworld draws, interiors and caves included.
- Menus, text boxes, and battles render flat on top, unaffected, and the
- infinite beyond-the-border-ring fill stays flat by design.
-- Collision, movement, sight lines, triggers, encounters, and scripts are
- untouched; nothing about the tilt reaches gameplay.
-
-## Colors mode
-
-The `2` key (and the Options menu COLORS row) cycles the display mode
-through **OG RED → SGB → ADVANCED → OG → OG INV → SGB INV → CLASSIC → OG RED**
-(on Blue the first slot labels **OG BLUE**; on Yellow, **OG YELLOW**).
-The first three are the real colorizations; the rest are DMG-shade novelties:
-
-- **OG RED** / **OG BLUE**: the Game Boy Color boot-ROM look for that cart --
- one global BG palette + one OBJ palette, every map, no per-map variation
- (Red/Blue ship no CGB code, so on a GBC the boot ROM colors them globally).
- The player/NPCs keep the boot-ROM OBJ color over the terrain via the OBP
- bake + post-zone redraw (`PaletteFX.GBC_BG` / `GBC_OBJ`, or Blue's blue/pink
- pair).
-- **OG YELLOW** (Yellow playthrough, same `ogred` save id): Pokemon Yellow's
- authentic GBC look from `CGBBasePalettes` (`data/palettes_yellow.lua`,
- sourced from pret/pokeyellow). Per-map / per-species colors, not a single
- boot-ROM ramp -- Yellow was CGB-enhanced.
-- **SGB** (default): the per-map Super Game Boy region palettes
- (`data/sgb/sgb_palettes.asm`). Sprites tint with the region palette, as on
- real SGB. (This is the mode formerly mislabeled "GBC".)
-- **ADVANCED**: pokered-gbc SuperPalettes -- real per-tile GBC coloring plus
- per-species mon colors (`data/palettes_gbc.lua`). (Formerly labeled
- "RED++"; it is the richest colorization rather than anything Red-specific.)
-- **OG**: force the four DMG grays (colorization off).
-- **OG INV**: inverted DMG grays.
-- **SGB INV**: each SGB zone palette with shade order reversed.
-- **CLASSIC**: original Game Boy pea-soup greens
- (`#9BBC0F` / `#8BAC0F` / `#306230` / `#0F380F`).
-
-The shade-remap transform is applied centrally in `PaletteFX.sendColors`, so
-it covers overworld, menus, battles, and tilt upright billboards. OG RED's
-global BG palette is supplied by `OverworldState:overworldBgColors` (per-map
-override in the overworld pass). Persisted as `save.options.colors`; the
-`gbc` / `gbc_inv` / `redpp` save ids are kept for back-compat under the new
-labels.
-
-## GBC FX
-
-The `5` key (and the Options menu GBC FX row) cycles a "played on real
-unlit-GBC hardware" post-process through **OFF → 1 → 2 → 3 → 4**. The
-levels are a cumulative ladder:
-
-- **1**: reflective-screen backing transparency.
-- **2**: + LCD pixel grid.
-- **3**: + pixel drop shadows.
-- **4**: + sunlight glare and rainbow shimmer with a drifting light.
-
-It runs as a final present pass after world + UI composite in
-`Renderer:endFrame`, inspired by the Pixel Transparency RetroArch shader
-([github.com/mattakins/Pixel_Transparency](https://github.com/mattakins/Pixel_Transparency)).
-Default OFF; persisted as `save.options.gbcfx`.
-
-Mobile GPUs often compile the pass but present a black frame, so Android and
-iOS hide the row entirely, pin the level to OFF, and rewrite a level already
-persisted in `options.lua` (issue #136). `POKEPORT_GBCFX` overrides that
-decision either way, same tri-state as `POKEPORT_TOUCH`: `=0` refuses the
-effect, `=1` forces it available. The Anbernic handheld pack exports `0` from
-its launcher because the device reports `"Linux"` while its GPU is in the
-phone class (see [Anbernic RG34XXSP](anbernic-rg34xxsp.md)).
-
-## Performance tier (low-end devices)
-
-The Options **PERFORMANCE** row scales the port's optional presentation
-extras down for weaker hardware. The extras it governs are the three
-heaviest things the port adds on top of the original -- the whole-screen 3D
-**TILT** (transforms the entire map as a ground plane), the **GBC FX**
-post-process shader (a fullscreen pass), and survey **ZOOM** (zooming out
-renders the connected neighbor maps, a lot of extra overdraw) -- plus a hard
-FPS ceiling. None of this touches game logic, which is fixed-step off `dt`
-(`src/core/FixedStep.lua`), so every tier plays identically; they differ
-only in how much eye-candy the renderer is allowed to do.
-
-| Tier | TILT | GBC FX | Survey ZOOM | Extra FPS ceiling |
-| ------------ | ---- | ------ | ----------- | ----------------- |
-| **HIGH** | on | on | on | none |
-| **BALANCED** | off | off | on | none |
-| **LOW** | off | off | off | 60 |
-| **AUTO** | picks a default from the device (below) |||
-
-- **AUTO** (the default) reads the device once at boot: ARM Linux handhelds
- (e.g. the RG34XXSP) resolve to **LOW**, phones/tablets and very-low-core
- desktops to **BALANCED**, and everything else -- a normal desktop, and
- every existing `options.lua` that predates this option -- to **HIGH**,
- so the common case is unchanged. See `src/core/Performance.detect`.
-- AUTO only chooses the *default*; all four tiers are selectable, so a
- wrong guess is one row away from being overridden.
-- The clamps are applied **live** against your stored options and never
- rewrite them (`Game:applyOptions`), so a lower tier hides your TILT / GBC
- FX / ZOOM without forgetting them -- raising the tier restores exactly
- what you had. (This is why the TILT / GBC FX / ZOOM rows still show your
- saved choice on a clamped tier: it's your preference, waiting for a tier
- that can afford it.)
-- Persisted as `save.options.performance` (`auto` | `high` | `balanced` |
- `low`); unit-tested in `tests/engine/performance_tiers.lua`.
-
-## Peer-to-peer link play (lua-enet)
-
-Trades and link battles connect two copies of the game directly over
-lua-enet (ENet ships inside LÖVE, nothing to install, no server to run)
-on a reliable-ordered channel, replacing the original standalone Python
-room-code relay (`tools/relay_server.py`, deleted). HOST A GAME shows the
-host's LAN address (UDP 7777; `POKEPORT_LINK_PORT` overrides); JOIN A
-GAME enters it. Closing performs a graceful ENet disconnect so the final
-confirm/bye always lands; a vanished peer exits with "The link was
-broken." Internet play needs a forwarded UDP port or a VPN (deliberate
-tradeoff vs. the relay). Headless tests drive the protocol over an
-in-memory loopback (`Net.loopbackPair`); under LÖVE the same test file
-also exercises real UDP pairing.
-
-Red, Blue, and Yellow copies link with each other, as the real cable
-does. The compatibility fingerprint hashes only data a link mode can
-actually read, so Yellow's Dragonair/Dragonite catch-rate retunes (the
-only R/B/Y link-surface difference) no longer read as different games
-(issue #511). Moving the fingerprint is a link parity change: builds
-from before this fix will refuse to pair with builds after it.
-
-## Fair play in link and online matches
-
-A link session is decided by the battle and nothing else, so for its
-duration:
-
-- **Game speed is pinned to normal.** The GAME SPEED option and
- `POKEPORT_SPEED` are ignored from the moment LINK PLAY opens until it
- closes, and apply again after. Fast-forward otherwise runs one peer's
- queue faster than the peer it is locked to and drains a tournament shot
- clock faster than the opponent racing it.
-- **Online play runs vanilla, except for your language.** Picking ONLINE
- MATCH or TOURNAMENT with mods enabled offers to switch the gameplay ones
- off and relaunch (mods merge at boot, so a restart is the only way). The
- restart is confirmed, not silent. They stay listed as disabled, ready to
- switch back on. A mod that declares itself a translation and provably
- writes nothing but text stays on: the two games hash the same link
- surface, so a Spanish install and an English one can battle and trade,
- each reading the game in its own language and naming the other player's
- party out of its own text.
-- **Only a meaningful split ends a match.** The per-turn state signature
- both peers exchange is split three ways: `actives` and `bench` carry
- species, HP, status, stat stages, PP and the rest of the party, and a
- divergence there ends the match as a draw. `volatile` carries per-turn
- flags both sides recompute anyway - a divergence there is logged and
- reported to mods, and play continues.
-
-The relay logs which component diverged on which turn, so a desync report
-names something specific.
-
-## Custom boot text
-
-The boot sequence replaces the Nintendo / GAME FREAK identifiers with
-"bois club" / "bryanthaboi", a deliberate branding customization. The
-rest of the boot beats (copyright splash, "presents" shooting-star, the
-Nidorino-vs-Gengar attract scene) mirror the original.
-
-
-## Custom Options
-
-Options persist in a standalone `options.lua` (separate from the game
-progress `save.lua`), so audio/display/battle preferences survive New Game
-and aren't wiped when a save slot is cleared. Changing a row in the Options
-menu or cycling hotkeys `2`/`3`/`4`/`5` writes immediately; an in-game save also
-flushes the live options. Old saves that still embed an `options` table are
-migrated once into `options.lua` on load.
-
-- Music / SFX volume
-- PIKACHU VOL (0-7, Yellow only): trims Pikachu's PCM voice clips under the
- SFX level, so the follower's constant chatter, the title-screen cry and
- every in-battle "Pika!" can be pulled down (or muted at 0) without
- quieting the rest of the sound effects. The row is hidden on Red/Blue,
- which have no voice clips.
-- Music Filter
-- OG GLITCHES on / off (Gen 1 quirks vs. modern-clean battle rules)
-- BATTLE LAYOUT (OG / WIDE); see "Widescreen battle layout" below
-- COLORS (OG RED / SGB / ADVANCED / OG / OG INV / SGB INV / CLASSIC), also
- hotkey `2` (OG RED = GBC boot-ROM look; ADVANCED uses pokered-gbc
- SuperPalettes + per-species mon colors)
-- TILT (OFF / 15 / 35 / 50), also hotkey `3` while free-roaming
-- ZOOM (FIT / OUTn / INn), also hotkey `4` while free-roaming; wheel and
- `-`/`=` step one level and save
-- VOID FILL (TREES / WATER / BLACK) for OVERWORLD beyond-edge space
-- GBC FX (OFF / 1 / 2 / 3 / 4), also hotkey `5`
-- MAX FPS (30 / 40 / 50 / 60 / 75 / 90 / 100 / 120 / 144 / 160, default 60),
- a hard render frame-rate cap (`save.options.fpsCap`).
-
-## Battle transition cascade + white battle letterbox
-
-Into-battle wipes still run the original eight styles inside the classic
-160×144 letterbox. On wide/tall windows (survey zoom), matching black 8×8
-blocks cascade outward from that square into the surrounding world so the
-void outside the OG wipe fills in lockstep. Once the battle state is up,
-letterbox voids around the battle canvas fill **white** instead of black
-so the whole window reads as one continuous battle screen.
-
-## Widescreen battle layout
-
-Options **BATTLE LAYOUT** picks the battle screen's composition: **OG**
-(the default: the original 160×144 arrangement, unchanged) or **WIDE**,
-which gives battles a 304×144 native-pixel surface and a Gen 3-style
-arrangement on it:
-
-- the foe's status box upper left, the foe's picture upper right;
-- the player's picture lower left, the player's status box lower right,
- with a longer HP bar and the numeric HP under it;
-- a full-width message window;
-- a split "What will X do?" prompt / 2×2 command window;
-- a 2×2 move menu, navigated with all four directions, with a PP and type
- panel attached to its right.
-
-Only the composition changes. Pictures, palettes, HP-bar colors, font
-pages, window borders, sounds, animations, timing and every battle rule
-stay the engine's, so a COLORS mode or an asset mod still owns the look.
-Each side's picture keeps its original pixels and placement math and is
-composited into its own region of the wider battlefield -- nothing is
-scaled or squeezed -- and animations, which are authored in the original
-160-pixel space, shift as one rigid group onto whichever side they play
-on. The whole screen is drawn at the window's integer fit scale for the
-wider surface, so a 304-pixel screen is drawn a step smaller than a
-160-pixel one in the same window.
-
-The wide surface is live only while the battle itself is the screen on
-top: a party menu, the bag or a nickname prompt is a 160×144 screen and
-brings the classic surface back with it.
-
-## On-screen touch controls (mobile)
-
-On Android/iOS the game draws a translucent d-pad (bottom-left), A/B
-buttons (bottom-right, Game Boy diagonal), and +/- START/SELECT (bottom
-center) over the frame, using Xelu's CC0 controller prompts
-(`assets/touch/`). Real buttons, not gestures: press lands the frame the
-finger does, sliding on the d-pad changes direction without lifting, and
-multi-touch chords (e.g. hold a direction + tap B) work. The overlay only
-appears while no controller is being used: the first gamepad button or
-stick push hides it, the next screen touch brings it back, and unplugging
-the last controller restores it immediately. Layout re-derives from the
-window size on rotation. Desktop testing: `POKEPORT_TOUCH=1 love .` forces
-the overlay on and lets the mouse act as a finger (`=0` forces it off).
-
-The launcher's **Touch Controls** button opens a drag editor: move each
-button freely, resize the whole pad with **-/+** (60% to 160%), **Disable**
-to hide the overlay permanently (for controllers / emulation handhelds --
-distinct from the temporary gamepad auto-hide), **Reset** for defaults,
-**Done** to save into `options.lua` as normalized window fractions so a
-different screen keeps the relative placement.
-
-Portrait and landscape are edited and saved separately (#633): the editor
-follows whichever orientation is on screen, and **Reset** only clears that
-one, so a layout that works held upright does not have to double as the
-one used sideways. An `options.lua` from before this split keeps its single
-layout in both orientations until one of them is edited. In-game, Options →
-**TOUCH PAD** toggles the same on/off flag without leaving a play session.
-
-## Screen orientation lock (Android)
-
-Options → **ORIENTATION** (also in the launcher's gear menu) locks the
-screen to **PORTRAIT**, **LANDSCAPE** (either landscape, following the
-device), or **REVERSE LANDSCAPE**, or leaves it on **AUTO** (#592). AUTO
-allows every orientation but defers to the system: with auto-rotate turned
-off in Android's quick settings, the game stays put instead of following
-the sensor (#716). Changes apply immediately -- the screen rotates as the
-row is stepped -- and persist in `options.lua`. Android only: iOS follows
-the app's fixed orientation list, and desktop windows rotate nothing.
-
-## Translation support
-
-Every string the player can read is now reachable from a mod, so a
-translation is an ordinary content mod rather than a fork.
-
-Two things had to change. Text layout stopped counting bytes: the dialogue
-box measures a line in glyphs (charmap sequences), so a 3-byte character
-costs one column, a cut never lands inside a character, and a page with a
-non-default `advance` re-measures instead of overflowing. That also fixed
-25 vanilla English lines that were wrapping early because `é` in POKéMON
-and POKéDEX costs two bytes ("I study POKéMON as" is 19 bytes and 18
-glyphs, and the box was breaking it).
-
-Second, the text the engine writes itself - battle messages, item results,
-menu labels, the link-play screens - moved behind `src/core/Strings.lua`
-and the new `strings` registry. Extracted script text was already
-overridable through `text`; this covers the other half. Entries are keyed
-by the English source, so a translation that has not reached a string yet
-keeps rendering in English and a half-finished translation stays playable.
-
-Authors generate the whole thing:
-
-```sh
-python3 tools/modkit.py translation francais --language "Francais"
-```
-
-That scaffolds a mod with every translatable string as an empty catalog,
-plus a glyph-page and charmap stub, a naming-grid stub, and a
-`francais-worksheet/` directory holding the English to translate from
-(deliberately outside the mod: extracted text is ROM content and must not
-be packed). `--refresh` re-harvests after an engine update, keeping
-existing translations and parking orphaned keys rather than dropping them.
-
-See the wiki's Translations guide.
-
-## Save editor (bundled, reachable from the launcher)
-
-The save editor ships inside every build instead of being a developer-only
-script, and the launcher's SAVE SLOT card grows an **Edit** label next to
-Delete on every slot that actually holds a save. Edit suspends the
-launcher, opens that slot's file in the editor, and **Close** hands the
-process back to the launcher with the slot list re-read (a rename, a badge
-or a dex change shows up on the row immediately). Unsaved edits arm a
-confirm first, so leaving cannot lose work. `love . --editor` still opens
-it standalone, where Close quits instead; `--save ` points it at any
-file, and a save can be dragged onto the window.
-
-The editor now wears the launcher's visual language - the same navy radial
-field, 16px translucent cards, tri-colour version rail and green/yellow/red
-semantics - so the two windows read as one app. Six tabs:
-
-- **Party**: the roster with sprites, HP bars and level chips on the left,
- and the mon inspector permanently docked on the right instead of floating
- over the list. Species, level, DVs and moves all round-trip through the
- Gen 1 formulas, so the inspector can never show illegal stats.
-- **Boxes**: the 12 PC boxes as a 5x4 grid with a fill meter per box and a
- party dock, so deposit and withdraw live in one place. Empty slots are
- clickable and create a mon there.
-- **Items**: money, a searchable item picker (replacing the arrows that
- cycled one id at a time through ~250 items), the configurable bag (20 slots
- by default), PC storage
- with no slot cap, and the eight badges as toggle chips. The picker, the bag
- and PC storage all scroll under the mouse wheel, so the whole catalog is
- reachable one-handed without typing a query.
-- **Events**: flags, defeated trainers, taken items and per-map object
- toggles, with a real filter field and a two-column paged grid.
-- **Map**: any map rendered with the game's own renderer, warps followable,
- and the player / lastHeal / lastOutdoor spawn points settable by clicking
- a cell. Setting lastOutdoor on a map the game would not accept as an
- outdoor source is refused with the reason.
-- **Dex**: seen / owned completion meters and a four-column grid; owning
- implies seen and un-seeing clears owned, exactly as the game requires.
-
-Two rules run through all of it. Every mutation goes through one funnel
-that sets the dirty flag and writes the status line together, so nothing
-changes silently and no branch can quietly no-op - "Party is full", "Bag is
-full", "click a cell first" all say so. And every destructive verb (Remove,
-Release, Clear all, Wipe dex) arms on the first click and commits on the
-second, relabelling itself to `Confirm?` in between.
-
-A validation pill in the tab rail mirrors what the running game would
-quarantine on load; clicking it jumps to the tab holding the first problem.
-
-## Tiled map editing (mod authoring)
-
-`tools/tiled_export.py` turns the imported ROM cache into a Tiled workspace,
-so maps can be edited in a real map editor and exported back out as a mod.
-It has its own document: docs/tiled-map-editing.md.
-
-## Pokédex diploma (both versions)
-
-The Celadon Mansion 3F game designer shows the dex-completion diploma
-once 150 species are owned. On Yellow, the graphic artist next to him
-then offers to print it, saving the certificate as a PNG under `prints/`
-in the save directory, and Bill's PC gains Yellow's PRINT BOX item which
-exports the current box list the same way.
-
-## Pokédex printing (Yellow)
-
-Yellow's Game Boy Printer PRNT option in the Pokédex side menu is stood in
-for by an image export: choosing PRNT renders the mon's entry page (sprite,
-kind, number, height/weight, dex text) to a PNG at 4x scale under
-`prints/` in the save directory, then reports the filename in a dialog.
-No printer hardware or link cable emulation involved; the file is the
-printout.
-
-## Find Mods (community mod indexes)
-
-A FIND MODS tab sits beside MODS in the launcher and browses a published
-mod index: a metadata-only feed listing mods that live in their authors'
-own repositories. No index ships with the launcher and none is ever added
-automatically, so the tab opens on an "Add an index" prompt until you name
-one; paste an index URL or its `owner/repo` and it is remembered in
-`options.lua`. More than one index can be added, and the listings merge.
-
-A feed author can publish per-mod release stats by adding three optional
-fields to an entry -- `downloads` (total across every release), and
-`first_release` / `last_release` (ISO days) -- which the listing shows in
-the same gold line the MODS tab uses. When a feed does not carry them,
-the row fetches the mod's own GitHub releases instead -- the same cached
-`ModUpdate` fetch the MODS tab uses, one entry per frame -- so the stats
-appear for any mod with a `github` field regardless of feed maintenance.
-The fields are additive: feeds that carry them stay readable by every
-build that predates them, and feeds that do not render exactly as before.
-
-## Soft reset (all versions)
-
-Holding A, B, START and SELECT together restarts the game the way flicking
-a Game Boy's power switch did, dropping straight back to the title screen.
-It works from anywhere, including mid-battle, which the QUIT entry on the
-start menu cannot do: the original combo is how stationary and gift
-Pokemon get their stats rerolled without sitting through a full relaunch.
-Unsaved progress is discarded, exactly as on hardware.
-
-As on the original, the four buttons have to stay held for 16 straight
-polls (better than a quarter of a second) and any direction in the mix
-cancels it, so it is hard to hit by accident -- including on the on-screen
-touch controls, where it would take four fingers held on four separate
-controls.
-
-## Controls rebinding (CONTROLS screen)
-
-OPTIONS -> CONTROLS lists every Game Boy button with its current keyboard
-key and controller button side by side (Z/A). Press A on a row, then press
-and release the key or pad button you want; the rebind commits on the
-release. If that input already belongs to another row, the two rows swap,
-so no button is ever stranded without an input and no input ever serves
-two buttons. Holding a second key or pad button while the first is still
-down backs out of the capture without touching a keyboard; Escape still
-cancels too. SELECT clears one row back to its default, and START resets
-every binding after a confirmation.
-
-Controllers a system has no mapping for (common on Linux handhelds and
-off-brand pads) report bare button numbers rather than names. Those are
-rebindable on the same screen and show up as JOY1, JOY2 and so on in the
-controller column. Recognized controllers are read only through their
-named buttons, so a rebind on those is never shadowed by the factory
-layout underneath it.
-
-## Mod profiles (#593)
-
-The mod manager's PROFILES tab holds named setups. A profile remembers which
-mods are on, every mod's own options, and which save slot each game version
-plays, so swapping profiles swaps the whole playthrough and not just the mod
-list. The setup that existed before profiles shipped becomes PROFILE 1 the
-first time the manager opens.
-
-EXPORT.. writes the selected profile to `profiles/.g1rmodlist` in the
-save directory; drop a `.g1rmodlist` someone shared into that folder and
-IMPORT.. adds it. Imported profiles never overwrite an existing one (a name
-clash gets a number). Mods the shared profile names but that are not installed
-are reported when the profile is applied; installing them is still a manual
-trip through the mods list or Find Mods.
-
-## Windows: no console windows on launcher actions
-
-Checking for updates, browsing a mod index, adding a mod repo, installing a
-mod and picking a ROM all run a host tool (curl, PowerShell) in a child
-process. On Windows those children used to each open their own console
-window, so a session could end up buried under half a dozen of them. The
-game now claims one console for itself at boot and hides it; the children
-inherit that invisible console and nothing pops up. Nothing else changes:
-file pickers are ordinary desktop dialogs and still appear normally, and a
-run started from a terminal (`lovec.exe`, what `scripts\run.ps1` prefers)
-keeps its terminal and its printed output. Set `POKEPORT_CONSOLE=1` to opt
-out.
+# New Features
+
+Features intentionally added beyond the original Pokémon Red, Blue, and Yellow games:
+
+* **Survey zoom** with connected-map rendering and configurable void fill
+* **Perspective tilt mode** for an HD-2D-style overworld
+* **Multiple color modes**, including original, SGB, advanced GBC, monochrome, and classic green
+* **Optional GBC screen effects**, including pixel grids, shadows, glare, and transparency
+* **Performance presets** and configurable FPS limits
+* **Peer-to-peer link play** for trades and battles between Red, Blue, and Yellow
+* **Persistent custom options** stored separately from game saves
+* **Optional widescreen battle layout**
+* **Mobile touch controls** with editable layouts, vibration, and orientation settings
+* **Translation and custom font support**
+* **Built-in save editor** for parties, boxes, items, events, maps, and Pokédex data
+* **Tiled map editing tools** for mod authors
+* **Pokédex diploma and printer image exports**
+* **Community mod browser**
+* **Soft reset button combination**
+* **Keyboard and controller rebinding**
+* **Mod profiles** with separate mod settings and save slots
+* **Improved launcher and save editor UI**, including background downloads and update checks
+* **Direct-launch options** for shortcuts, Steam entries, and handheld frontends
+* **Custom boot branding**
+
+Actual approximations, and missing original behavior are documented separately in `docs/known-differences.md`.
diff --git a/docs/required-to-function.md b/docs/required-to-function.md
index b6b6ea2a..bc681949 100644
--- a/docs/required-to-function.md
+++ b/docs/required-to-function.md
@@ -1,12 +1,11 @@
# What This Port Requires
The packaged desktop app requires one user-supplied input on first boot: a
-canonical 1 MiB US Pokemon Red ROM.
+canonical 1 MiB US Pokemon Red, Blue, or Yellow ROM.
-The importer verifies SHA-1
-`ea9bcae617fdf159b045185467ae58b2e4a48b9a`. Other revisions, Virtual
-Console releases, and Pokemon Blue are rejected rather than decoded with
-incorrect addresses.
+The importer verifies the SHA-1 for the game (see `src/core/GameVersion.lua`
+for specific hashes). Other revisions and Virtual Console releases are rejected
+rather than decoded with incorrect addresses.
After verification, the app generates its private cache in the LÖVE save
directory. It does not keep a copy of the ROM. Later boots use the cache.
@@ -15,9 +14,11 @@ Python and Pillow are not required by the packaged app.
## Bundled Metadata
Assembly removes high-level names and some relationships that the Lua port
-needs. `tools/rom_manifest.json` therefore contains:
+needs. The version-specific files `tools/rom_manifest.json`,
+`tools/rom_manifest_blue.json`, and `tools/rom_manifest_yellow.json` therefore
+contain:
-- the 3,268 ROM symbol addresses actually read by the extractor
+- the ROM symbol addresses actually read by the extractor
- symbolic IDs and ordering for maps, species, moves, items, and trainers
- source-erased dimensions, image names, and map object integration names
- hand-ported field/script integration tables
diff --git a/docs/rfcs/0002-screen-render-visible.md b/docs/rfcs/0002-screen-render-visible.md
new file mode 100644
index 00000000..da546bde
--- /dev/null
+++ b/docs/rfcs/0002-screen-render-visible.md
@@ -0,0 +1,54 @@
+# RFC 0002 — Let mods hide an active screen state from the main render
+
+## Status
+
+Proposed. Engine: `StateStack.lua`, `Game.lua`. Tests:
+`screen_render_visible.lua`.
+
+## Motivation
+
+A mod can render a native menu on a companion display through
+`render.compose`, but it cannot remove that menu from the main display without
+also popping it. Popping transfers update and input ownership and forces the
+mod to reimplement native menu behavior.
+
+## The decision it extends
+
+No prior D-number. Extends the render-hook plan in `docs/modding.md` and the
+state-stack rendering contract in `docs/architecture.md`.
+
+## The exact API delta
+
+Backward-compatible, additive-only.
+
+### `screen.render_visible`
+
+New hook called with `(state) -> boolean` through the public wrapper signature
+`(next, state)`. Its vanilla result is `true`.
+
+Returning `false` excludes the state from the main draw, from opaque-base
+selection and from palette-zone ownership. It does not remove the state or
+change update, input, push or pop behavior. The call sites are
+`StateStack:visibleBase`, `StateStack:draw` and the equivalent draw and palette
+walks in `Game:draw`.
+
+The hook is guarded by `Runtime.wantsHook`, so the no-subscriber path allocates
+nothing. It is a pure render predicate and may be evaluated more than once per
+frame.
+
+## Migration note for existing mods
+
+**Nothing.** With no subscriber every state remains visible, and the existing
+state-stack, event and hook behavior is unchanged.
+
+## Parity tests
+
+- **No-mod:** the topmost opaque state still owns drawing and palette zones,
+ and `Runtime.wantsHook("screen.render_visible")` stays false.
+- **Mod-API:** a fixture mod registers through `mod.hooks:wrap`, hides one
+ opaque state and proves the state beneath draws and owns the palette while
+ the hidden state remains topmost and continues updating.
+
+## Deprecation etiquette
+
+Nothing deprecated. This is one additive hook with a `true` vanilla default.
diff --git a/docs/rfcs/0003-multiplayer-session-layer.md b/docs/rfcs/0003-multiplayer-session-layer.md
new file mode 100644
index 00000000..66af086a
--- /dev/null
+++ b/docs/rfcs/0003-multiplayer-session-layer.md
@@ -0,0 +1,114 @@
+# RFC 0003 — Add a reusable multiplayer session layer
+
+## Status
+
+Proposed. Engine: `Session.lua`, `Net.lua`, `LinkState.lua`,
+`Tournament.lua`. Tests: `link_session.lua`.
+
+## Motivation
+
+Link play and tournaments currently own transport lifecycle details and
+temporarily remove and reinsert packets in `Net.inbox` when a handshake or
+battle starts. That makes packet ownership fragile and gives a future
+shared-world mode no stable host/guest-aware boundary to reuse.
+
+The engine needs one small layer that preserves today's wire protocol while
+owning received-packet order and terminal cleanup. Pokémon, battle, tournament,
+save, and overworld rules remain outside that layer.
+
+## The decision it extends
+
+Extends the existing split between `Net` (backend setup, framing, and relay
+controls), `Handshake`/`Protocol` (mode payloads), and the states that
+interpret those payloads. It does not replace any of those components.
+
+## The exact API delta
+
+Backward-compatible and internal-only.
+
+### `Session.new(transport, options)`
+
+Wraps one successfully configured Net-compatible transport. `options.role`
+is exactly `"host"` or `"guest"`; `options.kind` is a non-empty
+local label such as `"link"` or `"tournament"`. Role and kind are
+immutable session metadata selected locally and are never inferred from peer
+packets.
+
+The facade forwards the narrow fields current consumers need:
+`paired`, `code`, `address`, `target`,
+`error`, and `closed`.
+It forwards valid outbound tables unchanged through `send(message)`.
+
+### Receive and lifecycle methods
+
+- `update()` pumps the transport, validates decoded inbound values, and
+ appends accepted messages to a private FIFO.
+- `pollOne()` removes the oldest queued message.
+- `poll()` removes every queued message in order.
+- `take(type)` removes the first queued message with that type without
+ disturbing any other message.
+- `hasPending()` reports whether the FIFO is non-empty.
+- `getRole()`, `getKind()`, `getStatus()`, and
+ `getFailure()` expose local metadata and lifecycle.
+- `close()` closes the underlying transport once and is safe to repeat.
+
+Statuses are `connecting`, `paired`, `draining`, `closed`,
+and `failed`. A transport close or failure becomes `draining` while
+accepted packets remain queued. The terminal `closed`/`error`
+compatibility projection appears only after that FIFO drains, so a last packet
+travelling with a disconnect remains observable.
+
+An inbound value is structurally valid only when it is a table with a string
+`type`. Invalid decoded values end the session with a protocol failure.
+Unknown but structurally valid types remain queued for the owning mode; the
+session does not contain a packet allowlist.
+
+## Authority direction
+
+A later `WorldSession` may compose this facade. In that mode the host
+will own the world snapshot, map state, NPC state, event results, and shared
+progression. A guest will bring a trainer identity plus their Pokémon party,
+inventory, and other explicitly selected profile snapshot.
+
+Guest profile data and commands will be untrusted input. The host must validate
+them and must authorize every world mutation before rebroadcasting the result.
+The concrete snapshot schema, command vocabulary, conflict rules, and
+persistence policy require a separate RFC and are not introduced here.
+
+## Compatibility and security
+
+No packet envelope, message name, payload shape, framing rule, relay protocol,
+save schema, or engine protocol version changes. Existing valid outbound
+messages encode exactly as before, and existing link and tournament screens
+keep their current player-facing behavior.
+
+The layer does not authenticate players or encrypt traffic. Existing LAN and
+relay access assumptions remain unchanged; knowledge of a join address or code
+still grants the same access it grants today. Authentication, reconnect
+identity, rate limits, and abuse controls remain future protocol decisions.
+
+## Migration note for players, mods, and peers
+
+**Nothing.** `LinkState` and `Tournament` adopt the facade
+internally. Existing peers receive the same messages, mods gain no new API, and
+players do not migrate saves or settings.
+
+## Parity tests
+
+- **ROM-free facade:** constructor validation, immutable role/kind, unchanged
+ send shape, FIFO ordering, typed retrieval, unknown typed packets, draining,
+ terminal failure latching, protected transport calls, and decoded-value
+ rejection.
+- **Existing modes:** source guards prohibit direct inbox mutation; headless
+ module loads and the complete engine tier cover both migrated states.
+- **ROM-backed link play:** run the existing link driver when generated ROM
+ data is available; the normal quick suite remains the required baseline.
+
+## Deprecation etiquette and non-goals
+
+Nothing deprecated. This RFC adds an internal facade and removes no transport
+method.
+
+It does not add shared-world packets, co-op screens, a remote actor, save
+transfer, server persistence, matchmaking, reconnect, or a protocol-version
+bump. Those changes require the world-specific layer and its own review.
diff --git a/docs/rfcs/0003-playthrough-storage.md b/docs/rfcs/0003-playthrough-storage.md
new file mode 100644
index 00000000..96c574e2
--- /dev/null
+++ b/docs/rfcs/0003-playthrough-storage.md
@@ -0,0 +1,124 @@
+# RFC 0003 — Playthrough-scoped mod storage
+
+## Status
+
+Proposed. Engine: `SaveData.lua`, `SaveSerializer.lua`, `Storage.lua`,
+`Loader.lua`. Tests: `playthrough_identity.lua`, `storage.lua`, the existing
+save-slot and mod-save suites.
+
+## Motivation
+
+`mod.save` intentionally lives inside the normal progress record. That is the
+right home for quest state, but not for independent tool data such as replay
+captures, checkpoint histories, or recovery records: writing it would require a
+normal Pokémon SAVE, and storing copies of progress beneath `save.modData` would
+recursively embed the save that contains them.
+
+Mods also cannot safely infer which launcher slot or portable filesystem backs
+the active playthrough. Direct filesystem access would expose private paths and
+make isolation dependent on engine implementation details.
+
+## The decision it extends
+
+Extends the per-mod persistence contract documented in `docs/modding.md` and the
+wiki's Save Model. `mod.save` and `mod.options` keep their existing behavior.
+
+## The exact API delta
+
+Backward-compatible, additive-only. `Loader:_api` binds a new `mod.storage`
+facade to the calling mod id. Mods receive logical keys and decoded values, never
+filesystem handles or physical paths.
+
+### Lazy opaque playthrough identity
+
+`SaveData.ensurePlaythroughId(save[, fs]) -> id | nil` allocates an opaque
+32-hex-character identity without consuming gameplay RNG. It is called only when
+`mod.storage` or `mod.checkpoints` first needs a scope; New Game, ordinary SAVE,
+and ordinary load remain byte-compatible when no caller uses either API.
+
+The id is stored in `save.meta.playthroughId` after allocation. Until the next
+ordinary SAVE writes it into progress, a mapping in `options.lua` keeps legacy
+saves stable by game version and active launcher slot (or the legacy flat-save
+scope). A newly created playthrough never adopts the previous playthrough's
+mapping for that slot.
+
+`SaveData.persistenceFs([fs])` is engine-only routing used by the storage
+implementation. It follows the same standard/portable backend as progress and
+honors injected test filesystems; it is not exposed on the mod object.
+
+### `mod.storage:context(game)`
+
+Returns:
+
+```lua
+{ engineVersion = "0.9.0", gameVersion = "red", playthroughId = "..." }
+```
+
+or `nil, code, message`. `engineVersion` is warning-grade compatibility metadata;
+the context intentionally omits launcher slot ids and paths.
+
+### `mod.storage:write(game, key, value)`
+
+Accepts a data-only table and returns `true`, or
+`false, code, message`. Keys are nonempty slash-separated segments containing
+letters, digits, underscore, or dash. Empty segments, leading/trailing slash,
+`.`/`..`, and other characters are rejected.
+
+The engine encodes deterministically, stages and decodes a `.tmp` witness,
+preserves the previous valid generation, writes and decodes the main record,
+then rolls the verified bytes to `.bak`. A failed stage or replacement leaves a
+verified prior generation readable.
+
+### `mod.storage:read(game, key)`
+
+Returns a freshly decoded table, or `nil, code, message`. It tries main, staged,
+then backup data. A valid staged/backup value is returned and promoted
+best-effort; corrupt bytes are never executed.
+
+### `mod.storage:list(game[, prefix])`
+
+Returns sorted logical keys beneath a valid prefix, an exact key when the prefix
+names one, or `nil, code, message`. Physical witness filenames are hidden.
+
+### `mod.storage:delete(game, key)`
+
+Deletes only that key's main, backup, and staged witnesses. Returns `true`, or
+`false, code, message`.
+
+### Scope and errors
+
+Physical records are scoped as:
+
+`persistence root / mod_storage / game version / playthrough id / mod id`
+
+Stable error codes are `not_in_playthrough`, `storage_unavailable`,
+`invalid_key`, `encode_failed`, `write_failed`, `verify_failed`, and
+`not_found`. Ordinary data and I/O failures are return values, not callback-
+terminating errors.
+
+The restricted serializer's recursive writer runs outside LuaJIT traces. A
+1,000-process GC stress regression found compiled recursion could intermittently
+drop a newly inserted nested identity entry and produce undecodable bytes; save
+encoding is infrequent and I/O-bound, so interpreter execution is the safe
+boundary.
+
+## Migration note for existing mods
+
+**Nothing.** No API is removed, no manifest field changes, and no storage path or
+playthrough id is created unless a mod invokes `mod.storage` or
+`mod.checkpoints`. Existing save bytes remain unchanged on the no-caller path.
+
+## Parity tests
+
+- **No-mod:** New Game plus ordinary save/load creates no identity or storage
+ file; the existing save-slot and mod-save suites remain green.
+- **Engine identity:** lazy allocation, save/load preservation, stable legacy
+ mapping, fresh-playthrough replacement, and version/slot isolation.
+- **Public Mod API:** two real API-2 entry chunks prove data-only roundtrip,
+ deterministic listing, key rejection, mod/game/playthrough isolation,
+ corrupt-main recovery, failure retention, exact delete, and no-mod no-write.
+
+## Deprecation etiquette
+
+Nothing deprecated. The additions are one bound public facade and engine-private
+persistence/identity helpers.
diff --git a/docs/rfcs/0004-runtime-checkpoints.md b/docs/rfcs/0004-runtime-checkpoints.md
new file mode 100644
index 00000000..1df98749
--- /dev/null
+++ b/docs/rfcs/0004-runtime-checkpoints.md
@@ -0,0 +1,138 @@
+# RFC 0004 — Stable runtime checkpoints for mods
+
+## Status
+
+Proposed. Engine: `Checkpoint.lua`, `Game.lua`, `OverworldController.lua`,
+`Loader.lua`. Tests: `checkpoints.lua`, existing world and engine suites.
+
+## Motivation
+
+Mods can observe world events and request semantic actions, but no supported API
+can capture canonical progress at a proven-safe runtime boundary or reconstruct
+the overworld without replaying map-entry scripts. Reaching into the state stack,
+controller, ScriptRunner, or save restore internals would bind distributable mods
+to private objects and can duplicate story side effects.
+
+The engine is the only component that can authoritatively decide whether the
+runtime is settled and rebuild its controller objects. A generic checkpoint seam
+lets tools store data-only records while keeping those responsibilities private.
+
+## The decision it extends
+
+Extends the public world/tool surfaces in `docs/modding.md`. It does not change
+`mod.world`, normal CONTINUE, vanilla SAVE, or save lifecycle hooks/events.
+
+## The exact API delta
+
+Backward-compatible, additive-only. `Loader:_api` binds `mod.checkpoints`; mods
+never receive `Game`, StateStack, controller, coroutine, renderer, or filesystem
+internals inside a checkpoint.
+
+### `mod.checkpoints:inspect(game)`
+
+Returns a capability record. Stable overworld control returns:
+
+```lua
+{ canCapture = true, canRestore = true, kind = "overworld" }
+```
+
+A refusal returns the same booleans as `false` plus `kind`, `reason`, and a
+player-readable `message`. Format-1 supports only an overworld whose controller
+is topmost, player movement has settled on a tile, and no transition, foreground
+or parallel ScriptRunner, queued script, scripted move, engagement, emote,
+teleport, field animation, or similar partial controller mutation is active.
+
+Refusal reasons are `not_in_playthrough`, `not_overworld`, `screen_busy`,
+`transition_busy`, `script_busy`, `animation_busy`, and `movement_busy`.
+Identity allocation is lazy and happens only after an active topmost overworld
+has been established.
+
+### `mod.checkpoints:capture(game)`
+
+Returns a detached data-only format-1 checkpoint, or
+`nil, code, message`:
+
+```lua
+{
+ format = 1,
+ kind = "overworld",
+ identity = {
+ engineVersion = "...", gameVersion = "red", playthroughId = "...",
+ },
+ save = { -- canonical dynamic progress, excluding global options },
+ runtime = { overworld = {
+ map = "PALLET_TOWN", x = 5, y = 6,
+ facing = "down", surfing = false,
+ } },
+}
+```
+
+`engineVersion` is metadata for caller compatibility warnings; the engine does
+not reject patch/minor mismatches on restore. Capture deep-copies through the restricted serializer before and after
+`OverworldController:captureSave` synchronizes live map, tile, facing, and surf
+state. It excludes `save.options`, functions, userdata, threads, metatables as
+behavior, controller instances, and static content registries. Failure code
+`capture_failed` covers non-data progress and synchronization errors.
+
+### `mod.checkpoints:restore(game, checkpoint)`
+
+Returns `true`, or `false, code, message`. Before mutation it requires the current
+runtime to be capturable and validates a detached copy of the complete record:
+format, kind, internal identity consistency, current game/playthrough identity,
+map availability, integral in-bounds tile, facing, surfing, and synchronized save
+position.
+
+Validation codes are `invalid_checkpoint`, `invalid_content`, `unsupported_format`,
+`unsupported_runtime_kind`, `wrong_game`, `wrong_playthrough`, `invalid_map`, and
+`invalid_position`, in addition to the capability refusal reasons.
+
+The canonical save validator runs against the detached record. Unlike ordinary
+CONTINUE, a checkpoint never accepts a quarantine, remap, reclaim, clamp, or
+repair: any such content change returns `invalid_content` before live mutation.
+
+The engine captures an in-memory rollback checkpoint, preserves current global
+options, then reconstructs semantic overworld state through
+`Game:restoreCheckpointSave`. Checkpoint entry suppresses normal map exit/entry
+events, `onEnter` scripts, forced-movement/current checks, and last-map rewrites;
+it does not emit normal `save.loading`/`save.loaded` lifecycle events. After
+reconstruction, the engine recaptures and byte-compares normalized data. A failed
+apply rolls back and returns `restore_failed`; failure of that rollback returns
+`rollback_failed`. Only after a successful comparison does the engine emit
+`checkpoint.restored` with `{ game = game, kind = "overworld" }`. Validation
+failure, failed apply, and successful rollback emit nothing.
+
+Durable recovery remains a caller responsibility: in-memory rollback handles a
+runtime exception, not process termination.
+
+## Runtime boundary and future kinds
+
+This RFC's original Level A contract intentionally rejects battles, menus,
+transitions, animations, and suspended/queued scripts. RFC 0005 subsequently
+adds a separately inventoried `battle` kind with deterministic RNG and
+differential reconstruction tests; it does not broaden script or arbitrary-frame
+support implied here.
+
+## Migration note for existing mods
+
+**Nothing required.** No existing hook, save, controller, or world action changes
+when `mod.checkpoints` is unused. The reconstruction path is called only by a
+successful public restore after validation. Mods whose runtime caches derive from
+rewound `game.save` or `mod.save` state may optionally subscribe to
+`checkpoint.restored` and rebuild from their own public state.
+
+## Parity tests
+
+- **No-mod:** the complete ROM-free engine suite and existing world behavior stay
+ green; ordinary New Game/save/load allocates no checkpoint identity.
+- **Public Mod API:** a real API-2 entry chunk proves stable inspection and every
+ unsafe refusal, detached data-only capture, exact map/tile/facing/surf sync,
+ `A -> mutate B -> restore A -> recapture A2` equality across representative
+ progress, settings preservation, compatibility rejection without mutation,
+ map-side-effect suppression, injected reconstruction rollback, mod-owned
+ metadata and `mod.save` rewind, independent `mod.storage`/options preservation,
+ and success-only runtime-cache reconciliation through `checkpoint.restored`.
+
+## Deprecation etiquette
+
+Nothing deprecated. This adds one public facade and a checkpoint-only semantic
+reconstruction route.
diff --git a/docs/rfcs/0005-battle-runtime-checkpoints.md b/docs/rfcs/0005-battle-runtime-checkpoints.md
new file mode 100644
index 00000000..f94d2ba4
--- /dev/null
+++ b/docs/rfcs/0005-battle-runtime-checkpoints.md
@@ -0,0 +1,140 @@
+# RFC 0005 — Persistent battle safe-point checkpoints
+
+## Status
+
+Proposed. Extends RFC 0004. Engine: `BattleCheckpoint.lua`, `Checkpoint.lua`,
+`Game.lua`, `BattleState.lua`, and `OverworldController.lua`. Tests:
+`battle_checkpoint_*.lua`, `checkpoints.lua`, and the existing no-mod suites.
+
+## Motivation
+
+RFC 0004 lets a tool capture and reconstruct settled overworld progress without
+private engine access. A battle is a different runtime: its queue can hold Lua
+functions and UI factories, its controller contains renderer objects and live
+references, completion is currently an `onFinish` closure, and scripted battles
+resume a suspended `ScriptRunner` coroutine. Copying the controller would create
+a record that is neither data-only nor process-independent.
+
+The engine can instead expose a narrow semantic safe point. This gives all mods
+the strongest persistent battle checkpoint the current architecture can prove,
+without claiming mid-animation or suspended-script support.
+
+## API delta
+
+No new facade is added. The existing additive `mod.checkpoints` API gains a
+second format-1 runtime kind.
+
+### Capability
+
+`mod.checkpoints:inspect(game)` returns this only when an ordinary single-player
+wild or trainer battle is settled at the player command menu:
+
+```lua
+{ canCapture = true, canRestore = true, kind = "battle" }
+```
+
+The action/message queue, waits, UI, animations, HP/status presentation, and
+faint processing must be settled. The player must actually control the menu.
+The underlying overworld must have no running/queued script or scripted move,
+and the battle must carry an engine-owned semantic continuation descriptor.
+
+Additional refusal codes are `battle_phase_busy`, `battle_origin_unsupported`,
+`battle_variant_unsupported`, and `link_battle_unsupported`. Link, Safari,
+ghost, old-man/demo, fishing, static-object, script-suspended, and mod-created
+closure continuations remain rejected.
+
+### Capture
+
+A battle checkpoint remains detached and data-only:
+
+```lua
+{
+ format = 1,
+ kind = "battle",
+ identity = { engineVersion = "...", gameVersion = "red",
+ playthroughId = "..." },
+ save = { -- canonical dynamic progress, excluding global options },
+ runtime = {
+ overworld = { map = "ROUTE_1", x = 7, y = 8,
+ facing = "left", surfing = false },
+ battle = { -- normalized semantic model and continuation },
+ },
+ rng = { love = "..." },
+}
+```
+
+The model carries player/enemy roster indices, dynamic enemy Pokémon, turn and
+escape state, HP/PP/status/stages/volatiles, participants, level-up tracking,
+trainer AI state, battle ruleset identity, side/field extension data, and
+normalized pointer relationships such as multi-turn move slots and Mimic
+restoration entries. Definitions, sprites, canvases, queues, callbacks, and
+controller objects are reconstructed or excluded.
+
+Callback-bearing battle extension tokens fail with `battle_extension_unsafe`;
+invalid live reference relationships fail with `battle_state_invalid`. Nothing
+is silently stripped.
+
+New overworld checkpoints also carry the LÖVE gameplay RNG state. Legacy
+format-1 overworld checkpoints without `rng` remain loadable and leave the
+current stream untouched.
+
+### Restore
+
+Battle restore validates the detached save, map, content references, ruleset,
+roster indices, move references, continuation identity, and RNG before live
+mutation. The engine then:
+
+1. reconstructs the saved overworld return point without entry side effects;
+2. creates a fresh `BattleState` from current content registries;
+3. applies the normalized battle model and rebuilds object-reference relations;
+4. binds an engine-owned wild/trainer completion continuation;
+5. installs the battle directly at the settled menu without replaying its intro;
+6. restores the RNG after reconstruction has finished; and
+7. recaptures and compares the complete checkpoint; and
+8. emits `checkpoint.restored` with `{ game = game, kind = "battle" }` after the
+ comparison succeeds.
+
+The pre-operation checkpoint is the transaction rollback. A failed post-install
+RNG restore is covered: both battle runtime and RNG are reconstructed back to
+their original values. Validation failure, failed reconstruction, and successful
+rollback emit no checkpoint lifecycle event.
+
+## Continuation decision
+
+Ordinary random wild battles resume through `OverworldState:afterBattle`.
+Ordinary trainer battles use a descriptor containing map id, stable NPC id,
+trainer class/party, and optional header event; a win reapplies the same defeated
+flag, event, reward, and `afterBattle` path. Reconstructed overworld input and
+NPC freeze state are normalized instead of reviving the old closure.
+
+`Commands.start_battle` is deliberately unsupported: its completion closure
+mutates script context and resumes a coroutine whose program counter and Lua
+stack cannot be serialized. Existing script rejection remains the correct safe
+contract until a separate semantic ScriptRunner checkpoint RFC exists.
+
+## Migration note
+
+**Existing mods require no changes.** The facade and format number are unchanged;
+the new kind, RNG field, and success-only lifecycle event are additive.
+Overworld-only callers may continue to filter `capability.kind`. Mods with derived
+runtime caches may rebuild them from restored public state when the event fires.
+No-mod behavior is unchanged when checkpoints are unused.
+
+## Verification
+
+- settled/unsafe boundary and every variant refusal;
+- data-only wild and trainer capture, including callback-bearing extension
+ rejection;
+- process-independent controller and continuation reconstruction;
+- exact differential recapture for wild and trainer states;
+- HP, PP, status/stages/volatiles, AI layer, participants, enemy roster,
+ multi-turn move references, and Mimic restore pointers;
+- exact damage, critical, accuracy, random AI, escape, next encounter, and next
+ raw RNG result after reload;
+- corrupt content/continuation rejection before mutation;
+- injected post-install failure with full runtime and RNG rollback;
+- mod-added Pokémon metadata and `mod.save` rewind while independent
+ `mod.storage` and options remain current;
+- exactly one post-verification `checkpoint.restored` event and none on failure;
+- legacy overworld checkpoint compatibility;
+- complete ROM-free engine and public mod-API suites.
diff --git a/docs/rfcs/0006-platform-lifecycle-hooks.md b/docs/rfcs/0006-platform-lifecycle-hooks.md
new file mode 100644
index 00000000..9763fed1
--- /dev/null
+++ b/docs/rfcs/0006-platform-lifecycle-hooks.md
@@ -0,0 +1,107 @@
+# RFC 0006 — Generic process-lifecycle hooks for platform launcher integrations
+
+## Status
+
+Proposed. Engine: `PlatformHooks.lua` (new), `main.lua`, `Manifest.lua`,
+`Loader.lua`. Tests: `tests/modkit/cases/platform_lifecycle_hooks.lua`,
+`tests/mod_loader_tests.lua`, `tests/mod_manifest_tests.lua`.
+
+## Motivation
+
+A platform-specific launcher wrapper -- a native shell that embeds this
+engine and owns its own UI around the game window (a mobile app shell,
+say, presenting its own settings/import/save screens and only handing
+control to the LÖVE window once play starts) needs three things no
+current hook covers:
+
+1. Pause the simulation while its own UI is on top of the game window.
+2. Live-reload options it wrote from outside any Lua UI.
+3. Veto `main.lua`'s "closing the window returns to the Lua launcher"
+ behavior when the platform shell owns that job itself -- without this,
+ a shell that re-fronts its own launcher UI on quit gets looped straight
+ back into `HostShell.restart()`'s in-process reboot instead.
+
+Implementing this by hand-patching `main.lua`'s `love.update`/`love.quit`
+directly ties every such integration to editing the one file every other
+engine change also touches, guaranteeing merge conflicts for any second
+platform integration (or any unrelated engine PR landing around the same
+time). No existing hook covers "should the per-frame simulation step run"
+or "should closing the window return to the Lua launcher."
+
+## The decision it extends
+
+No prior D-number. Extends the hook-contract section of `docs/modding.md`
+alongside `input.step`, `render.hud`, `screen.render_visible`, etc.
+
+## The exact API delta
+
+Backward-compatible, additive-only.
+
+### `core.update`
+
+New hook, `(game, dt) -> nil` through the public wrapper signature
+`(next, game, dt)`, called once per frame from `love.update` via
+`src/core/PlatformHooks.lua`'s `PlatformHooks.update(game, dt)`. Vanilla
+behavior (used when no mod claims the hook) is `game:update(dt)`,
+unconditionally -- identical to `love.update`'s behavior before this hook
+existed. A subscriber may skip calling `next(game, dt)` to pause the
+simulation for that frame, or do additional per-frame work before/after
+calling it regardless of whether it calls `next`.
+
+### `core.quit_to_launcher`
+
+New hook, `() -> boolean` through the public wrapper signature `(next)`,
+called once from `love.quit()` via
+`PlatformHooks.quitToLauncher(vanilla)`. `vanilla` is the pre-existing
+non-platform-specific decision (`Game and not Importer and not
+quitToLauncher and not scripted and not launchedIntoGame`). A subscriber
+may return `false` outright to veto returning to the Lua launcher (without
+ever calling `next`, so the vanilla condition is never evaluated), or call
+`next()` and return its result to pass the vanilla decision through
+unchanged.
+
+Neither hook is guarded by `Runtime.wantsHook` -- both fire unconditionally
+every call, matching the existing `input.step` precedent
+(`src/core/Game.lua`), since `Hooks:call` already fast-paths to a bare
+`vanilla(...)` call when no mod has wrapped the name.
+
+### `Manifest.force_enable_env`
+
+New optional manifest field, a bare env-var name. `Loader:load` re-enables
+a mod carrying this field whenever that variable is set to `"1"`,
+regardless of a saved disable in `options.mods`. This exists for exactly
+the mod class this RFC is for: a platform-bridge mod that ships only with
+one build and cannot function disabled there, but must still behave like
+every other mod (a manifest opt-in, not an engine special case) on every
+build that doesn't set its variable.
+
+## Migration note for existing mods
+
+**Nothing.** With no subscriber, `love.update` still calls `Game:update(dt)`
+unconditionally every frame and `love.quit()`'s restart-to-launcher
+decision is exactly the pre-existing condition -- bit-identical to today's
+behavior on every platform where no mod wraps either hook. A manifest with
+no `force_enable_env` field behaves exactly as before.
+
+## Parity tests
+
+- **No-mod:** `core.update`'s vanilla runs exactly once per call with the
+ hook chain empty; `core.quit_to_launcher`'s vanilla return value passes
+ through unchanged. Both hooks are picked up automatically by the
+ catalog-driven no-mod gate (`tests/engine/gate_hooks.lua`, which scans
+ for `Runtime.call("...")` call sites), so neither needs a dedicated
+ no-mod test file.
+- **Mod-API:** `tests/modkit/cases/platform_lifecycle_hooks.lua` proves,
+ through a fixture mod loaded via the public loader (not the engine's
+ internals), that a subscriber can skip the vanilla update call (pause),
+ run extra per-frame polling regardless of pause state, and veto the
+ quit-to-launcher decision without the vanilla condition ever running.
+- `tests/mod_loader_tests.lua` and `tests/mod_manifest_tests.lua` cover
+ `force_enable_env`: a matching env var re-enables a mod saved as
+ disabled, and an unset one leaves the saved disable alone.
+
+## Deprecation etiquette
+
+Nothing deprecated. These are two additive hooks and one additive manifest
+field; `main.lua`'s only footprint is one `require` and two call sites
+into `src/core/PlatformHooks.lua`.
diff --git a/docs/rfcs/0007-per-category-game-speed.md b/docs/rfcs/0007-per-category-game-speed.md
new file mode 100644
index 00000000..eb34b3a4
--- /dev/null
+++ b/docs/rfcs/0007-per-category-game-speed.md
@@ -0,0 +1,212 @@
+# RFC 0007 — Per-category GAME SPEED and the `core.logic_speed` hook
+
+## Status
+
+Proposed. Engine: `GameSpeed.lua`, `Game.lua`, `BattleState.lua`,
+`OptionsMenu.lua`, `SaveData.lua`, `LauncherSettings.lua`. Tests:
+`tests/engine/game_speed_categories_test.lua`,
+`tests/engine/gate_hooks.lua` (structural, automatic), `tests/run_tests.lua`
+(OptionsMenu row walk), `tests/mod_ui_tests.lua` (row id/order).
+
+## Motivation
+
+`GameSpeed` (`src/core/GameSpeed.lua`) is a single fast-forward multiplier
+applied uniformly to the whole logic clock in `Game:logicSpeed()` /
+`Game:update()` -- overworld walking, menu navigation and battle turns all
+scale together. A player who wants 4X battles (grinding, a long gym fight)
+but 1X overworld (so a scripted cutscene or NPC dialogue doesn't blur past)
+has no way to get both; the one GAME SPEED row is a single ladder that
+applies everywhere at once.
+
+This needs to be an engine change, not a mod: there is no per-frame seam a
+mod can use to swap the multiplier mid-step, and no public event granular
+enough to say "which category is active" (`screen.pushed`/`screen.popped`
+and `battle.started`/`battle.ended` are the closest and are not enough --
+see Decisions below). The engine's own speed resolution has to become
+category-aware.
+
+A category-aware speed resolution is also the general seam a
+platform-launcher integration or automation tool needs to read or override
+the effective multiplier for a given frame without caring which category
+produced it -- this RFC's `core.logic_speed` hook is written for that case
+alongside the player-facing Options rows.
+
+## The decision it extends
+
+No prior D-number. Extends `GameSpeed.lua`'s multiplier ladder (unchanged)
+with per-category resolution.
+
+## The exact API delta
+
+Backward-compatible except for one save-data field rename, which ships with
+an automatic migration (see below) -- nothing in the public mod API (hooks,
+events, registries, `mod.*`) is renamed or removed.
+
+### `save.options`: `speed` -> `speedOverworld` / `speedBattle` / `speedMenu`
+
+`GameSpeed.CATEGORIES = { "overworld", "battle", "menu" }` is the new list
+of categories, and `GameSpeed.optionKey(category)` maps a category to its
+`save.options` field name (`"overworld"` -> `"speedOverworld"`, etc.).
+`GameSpeed.LEVELS`, `.DEFAULT`, `.levelLabel`, `.clamp` and `.cycle` are
+unchanged -- the ladder and its behavior are exactly what they were, just
+applied three times instead of once.
+
+`SaveData.defaultOptions()` drops `speed = 1` and adds `speedOverworld = 1`,
+`speedBattle = 1`, `speedMenu = 1`. `SaveData.mergeOptions()` migrates: a
+loaded options table that still has `speed` and none of the three new
+fields seeds all three from it, so an existing player's fast-forward
+preference carries over instead of two of the three categories silently
+resetting to 1X. `speed` is dropped on the way out (not carried forward),
+so a re-save never re-triggers the migration.
+
+### `Game.speedCategoryInStack(stack)`
+
+New static helper, `(stack) -> "battle" | "overworld" | "menu"`. Walks the
+whole state stack top-down -- the same idiom `Game.wideBattleInStack` and
+`Game.fillScaleInStack` already use -- looking for `state.isBattle` (new
+marker, `BattleState.isBattle = true`, covering every battle: wild,
+trainer, link, safari, the old-man demo) or `state.isOverworld` (existing
+marker, `OverworldController`'s `OverworldState.isOverworld = true`). The
+first match wins; a state with neither marker (a menu, a text box, a
+naming screen, a cutscene) is transparent to the walk and falls through to
+whatever is under it. Nothing in the stack matching either falls back to
+`"menu"`.
+
+### `Game:logicSpeed()` / `Game:_resolveLogicSpeed()`
+
+`Game:_resolveLogicSpeed()` is new: it resolves `Game.speedCategoryInStack`
+against the live stack, maps the category to its `save.options` key via
+`GameSpeed.optionKey`, and returns `GameSpeed.clamp` of that option (or
+`GameSpeed.DEFAULT`). This is the exact category-resolution logic the new
+hook wraps.
+
+`Game:logicSpeed()` keeps its existing early returns -- link play forces
+`1`, a run-argument speed override wins over the saved option -- unchanged,
+and in the same order, before ever calling the hook. Only once neither
+applies does it call the `core.logic_speed` hook.
+
+### `Game:_cycleSpeed(dir)`
+
+The keyboard hotkey and the gamepad shoulders/triggers that used to cycle
+the single `speed` option now cycle whichever category
+`Game.speedCategoryInStack` says is active: pressing the hotkey during a
+battle speeds up just the battle, on the overworld just the walk, in a menu
+just the menu. This is the natural per-category answer for a control that
+used to have one option to reach and now has three -- see Decisions below
+for why this reading was chosen over, say, always cycling `overworld`.
+
+### `core.logic_speed`
+
+New hook, `(game) -> number` through the public wrapper signature
+`(next, game)`, called once per `Game:logicSpeed()` (i.e. once per frame).
+Vanilla behavior (used when no mod claims the hook) is
+`Game:_resolveLogicSpeed()` -- exactly the category resolution above,
+nothing else. A subscriber may call `next(game)` and return its result to
+pass the vanilla multiplier through, or return a different number outright
+to override it for that frame (e.g. a bot mod forcing `1` during one route
+segment regardless of what category or option is active).
+
+This intentionally sits *after* the link and speed-override checks in
+`Game:logicSpeed()`, not around them: link play staying locked to 1X "no
+matter what either player set this to" is exactly the invariant that would
+break if a mod's hook could override it, and the run-argument override
+exists so a bot/screenshot run's speed does not depend on a mod any more
+than on the player's saved option. Both stay unconditional early returns a
+mod never sees.
+
+Not guarded by `Runtime.wantsHook`: `Hooks:call` already fast-paths to a
+bare `vanilla(...)` call when no mod has wrapped the name, and this hook
+fires every frame regardless.
+
+## Decisions on the issue's open questions
+
+**1. Overlays on top of another category's state (a party menu, a choice
+box, a naming screen opened mid-battle or mid-overworld).** Resolved by
+making the category a property of stack *position*, not of the overlay's
+own type: an overlay with no `isBattle`/`isOverworld` marker is transparent
+to `Game.speedCategoryInStack`'s walk and inherits whatever is under it. A
+party swap opened mid-battle reads as `"battle"`; a bag opened while
+walking reads as `"overworld"`. This was chosen over giving every UI state
+its own fixed category (which would make a fast-forwarded battle visibly
+stutter back to 1X every time its party menu opens) because it matches
+what the player is actually doing moment to moment, and it reuses a
+pattern the codebase already leans on for exactly this "menus opened over
+X should behave like X" class of problem (`Game.fillScaleInStack`,
+`Game.wideBattleInStack`).
+
+**2. Cutscenes/scripts.** No fourth category. A scripted sequence runs
+through the owning state's own machinery -- the overworld's script runner
+or a battle's message queue -- rather than pushing a state of its own, so
+it is already covered by decision 1: it inherits whatever category the
+state driving it resolves to. A cutscene state that genuinely has nothing
+under it (a pre-game intro) falls to `"menu"`, the default for anything
+that is not battle or overworld gameplay -- consistent with those being
+pre-game presentation, not something a player is likely to want scaled
+differently from menu navigation.
+
+**3. Category granularity (splitting "menu" further).** Deferred. Start
+with the three named here; `GameSpeed.CATEGORIES` and `GameSpeed.optionKey`
+are written so adding a fourth later (a Pokédex/Bag category, say) is one
+entry plus one new `save.options` field, not a resolution-logic rewrite.
+No current request motivates it.
+
+**4. The GAME SPEED hotkey/shoulder buttons, once "the" speed is three
+things.** `Game:_cycleSpeed` now cycles whichever category is currently
+active (`Game.speedCategoryInStack`), rather than, say, always cycling
+`overworld` or requiring a modifier key to pick a category. A single
+physical control that means "speed up whatever I'm looking at right now"
+is the reading that needs no new UI and matches what a player pressing it
+mid-battle almost certainly wants.
+
+## Migration note for existing mods
+
+**Nothing**, for the mod API surface: `content.X:register/override/get`,
+`events:on`, `hooks:wrap`, `mod.log`, `mod:read`, manifest v1 fields are
+untouched, and `GameSpeed.LEVELS`/`.DEFAULT`/`.levelLabel`/`.clamp`/`.cycle`
+keep their exact signatures and behavior.
+
+**One save-data field**, for anything that read `save.options.speed`
+directly (not a formal registry/hook surface, but worth naming): it is
+superseded by `speedOverworld`/`speedBattle`/`speedMenu`, migrated
+automatically on load (see above) so a save from before this RFC keeps its
+player's chosen speed. A mod reading `save.options.speed` after this change
+sees `nil` (the key is dropped on migration, not kept as a stale alias) and
+should read the per-category fields, or hook `core.logic_speed` to observe
+the resolved multiplier directly regardless of which category produced it.
+
+## Parity tests
+
+- **No-mod:** `core.logic_speed` needs no dedicated no-mod test file --
+ `tests/engine/gate_hooks.lua` walks the live hook catalog (which scans
+ `src` for `Runtime.call("...")` call sites), so the new
+ `Runtime.call("core.logic_speed", ...)` site is picked up and gated
+ automatically: vanilla runs exactly once with an empty hook chain, an
+ unsubscribed-but-live bus passes values and multiple returns through
+ unchanged, and `Runtime.wantsHook` reads `false`.
+- **Mod-API:** `tests/engine/game_speed_categories_test.lua` exercises the
+ hook through the public API (`Hooks.new()` + `bus:wrap("core.logic_speed",
+ ...)` + `Runtime.call`, the same idiom other hooks' tests use) -- a
+ subscriber can read the vanilla category resolution via `next(game)` and
+ can override it outright -- plus direct coverage of
+ `Game.speedCategoryInStack` (battle-on-top, overworld-on-top, an overlay
+ inheriting each, an empty/unmatched stack falling to `"menu"`) and
+ `Game:logicSpeed()`'s precedence (link forces 1X over all three
+ categories and over a hook override; the run-argument override wins over
+ the category resolution).
+- `tests/run_tests.lua`'s OptionsMenu walk exercises the three new rows
+ (OVERWORLD SPEED / BATTLE SPEED / MENU SPEED) cycling and wrapping
+ independently, in place of the old single GAME SPEED row.
+- `tests/mod_ui_tests.lua`'s row-id/order check and hardcoded row-index
+ activations (MODS, CONTROLS) are updated for the two extra rows.
+- A link-play driver should set all three per-category speeds high before
+ asserting `game:logicSpeed()` reads `1` during a real link session,
+ proving the lock wins over every category at once, not just whichever
+ one happens to be active.
+
+## Deprecation etiquette
+
+Nothing deprecated in the mod-facing hook/event/registry catalog -- this
+adds one hook, additive. The `save.options.speed` field is superseded with
+an automatic migration rather than a deprecation notice, since it was never
+a registered mod-API surface (no schema entry, no registry) -- the same
+treatment any other `save.options` field would get if it needed reshaping.
diff --git a/docs/switch-build.md b/docs/switch-build.md
index 74b33567..61f8a574 100644
--- a/docs/switch-build.md
+++ b/docs/switch-build.md
@@ -1,38 +1,66 @@
-# Build the Nintendo Switch NRO — contributor guide
+# Build Gen1Recomp for Nintendo Switch
Want to play a release build instead? Download the SD-ready zip and extract it
-at your microSD root — see [switch-install.md](switch-install.md).
+at your microSD root. See [switch-install.md](switch-install.md).
This guide is for contributors who build Gen1Recomp for Switch from source.
-Hardware evidence, MTP operator loops, and deeper notes live in
-[switch-development.md](switch-development.md).
-> Releases ship `gen1recomp-*-switch.zip` (SD tree under `switch/gen1recomp/`;
-> issue [#531](https://github.com/bryanthaboi/gen1recomp/issues/531)). Hardware
-> evidence: **OLED** (author) and **V1 boot** (community). See
-> [switch-development.md](switch-development.md) for known limitations.
+> Releases ship `gen1recomp-*-switch.zip` (SD tree under `switch/gen1recomp/`).
+> Runtime target is pinned [love-nx](https://github.com/retronx-team/love-nx)
+> `11.5-nx1`. Player install and limitations: [switch-install.md](switch-install.md).
---
## Prerequisites by OS
All packaging entrypoints are **bash**. On Windows, use Git Bash, MSYS2, or
-WSL — not cmd.exe or PowerShell (AD-008).
+WSL, not cmd.exe or PowerShell (AD-008).
### macOS / Linux
1. Install [devkitPro pacman](https://devkitpro.org/wiki/devkitPro_pacman).
-2. Install Switch tools:
+2. Install Switch tools (**required for `--fused`**):
```sh
sudo dkp-pacman -S switch-dev
```
-3. Ensure `nacptool` and `elf2nro` are on `PATH` (or under
- `$DEVKITPRO/tools/bin` — the fused script prepends that when set).
+3. OTA launcher toolchain, **native or Docker** (either is fine):
-**Optional:** Install [Docker](https://docs.docker.com/get-docker/) so fused
-builds can fall back to the pinned image when native tools are missing.
+ ```sh
+ bash scripts/switch/install_devkitpro_deps.sh # native
+ # or install Docker (same pin as fused builds)
+ ```
+
+4. Ensure `DEVKITPRO` is exported (typical macOS: `/opt/devkitpro`) and
+ `nacptool` / `elf2nro` are on `PATH` (or under `$DEVKITPRO/tools/bin`).
+
+Fused game builds can also use Docker when native `nacptool`/`elf2nro` are absent.
+
+### Native OTA launcher (included in `--fused`)
+
+In-console OTA uses a **separate DEVKITPRO NRO** (not LÖVE). The LÖVE
+self-updater (`Check.lua`) is disabled on NX. Source:
+`ports/switch/ota-launcher/`. Host protocol tests (no toolchain):
+
+```sh
+make -C ports/switch/ota-launcher host-test
+# or
+scripts/switch/build_ota_launcher.sh # host-test first; NRO needs DEVKITPRO/Docker
+```
+
+`--fused` always builds the fused game, native OTA launcher, and dual-NRO SD
+zip. The same `*-switch.zip` is the OTA download asset. **DEVKITPRO is
+required.** OTA launcher: native packages **or** Docker. Both are supported.
+
+Release-like build from repo root:
+
+```sh
+scripts/build_switch.sh --fetch --fused --version X.Y.Z
+```
+
+See `ports/switch/ota-launcher/README.md` and
+`scripts/switch/ota_launcher.manifest`.
### Windows (Git Bash / MSYS2 / WSL)
@@ -42,16 +70,15 @@ builds can fall back to the pinned image when native tools are missing.
- **WSL** (Ubuntu/etc.) with the Linux pacman flow above, or
- **Git Bash** for `--fetch` / `--loose`; for `--fused` prefer MSYS2 or
WSL if Docker bind-mounts from Git Bash paths misbehave.
-2. Install `switch-dev` (or rely on Docker fallback — see below).
+2. Install `switch-dev` (or rely on Docker fallback; see below).
3. Do **not** expect `scripts/build_switch.sh` to run under cmd/PowerShell.
### What you must install yourself
| You install | Script does **not** install |
| ----------- | --------------------------- |
-| bash, git, zip tooling the repo already expects | — |
-| `dkp-pacman` + `switch-dev` (native fused) | `dkp-pacman -S …` |
-| Docker (optional fused fallback) | Docker Engine |
+| bash, git, zip tooling the repo already expects | (none) |
+| `dkp-pacman` + `switch-dev` + OTA packages **or** Docker | `dkp-pacman -S …` |
| A legal `.gb` ROM (to play) | Any ROM or game data |
---
@@ -64,12 +91,12 @@ builds can fall back to the pinned image when native tools are missing.
| ---- | ------------ |
| `--fetch` | Downloads pinned **love.nro** + **love.elf** into `.bazinga/love-nx/11.5-nx1/` and verifies SHA-256 against `scripts/switch/love-nx-11.5-nx1.sha256`. |
| `--loose` | Packs `game.love`, copies pinned `love.nro` → `dist/switch/loose/` as `gen1recomp.nro` + `game.love` side by side. Needs the pin. |
-| `--fused` | Builds `dist/switch/gen1recomp--switch.nro` (game in romfs) via `nacptool` + `elf2nro`, then packs `dist/switch/gen1recomp--switch.zip` (SD-ready tree). Needs the pin + toolchain (native or Docker). GitHub Releases publish the **zip only**. |
+| `--fused` | Builds fused game NRO, OTA launcher NRO, and dual-NRO SD zip. **Requires DEVKITPRO** + `switch-dev`. OTA launcher: native packages or Docker. GitHub Releases publish the **zip only**. |
Rules:
- `--fetch` alone is fine; combine as `--fetch --loose` or `--fetch --fused`.
-- `--loose` and `--fused` are **XOR** — pick one packaging path per run.
+- `--loose` and `--fused` are **XOR**. Pick one packaging path per run.
- `--version X.Y.Z` sets the NACP / filename version (defaults to short git SHA).
### What `--fetch` downloads
@@ -109,13 +136,15 @@ scripts/build_switch.sh --fetch
# Loose pair for iteration (fetch + assemble)
scripts/build_switch.sh --fetch --loose
-# Single fused NRO + SD-ready zip for a release-like artifact
+# Fused game + OTA launcher + dual-NRO SD zip for a release-like artifact
scripts/build_switch.sh --fetch --fused --version 0.2.0
```
Outputs land under `dist/switch/` (and `dist/switch/loose/` for loose mode).
-The fused path also writes `gen1recomp--switch.nro.sha256` and
-`gen1recomp--switch.zip` (+ `.sha256` sidecar for the zip).
+The fused path also writes `gen1recomp--switch.nro` (game),
+`gen1recomp--launcher.nro`, `gen1recomp--game.nro`,
+`gen1recomp--switch.nro.sha256`, and `gen1recomp--switch.zip`
+(+ `.sha256` sidecar for the zip).
Offline packaging smoke (no network, no nacptool required):
@@ -143,7 +172,7 @@ the NX runtime modules `src/core/NxAssetOverlay.lua`, `src/core/Platform.lua`,
`tests/engine/switch_diagnostics_test.lua`, `tests/engine/platform_nx_*`,
or the Switch-related workflow YAML), CI runs:
-1. **Offline selftest** on `ubuntu-latest` (forks **and** the canonical repo):
+1. **Offline selftest** on `ubuntu-latest` (forks **and** the main repo):
`scripts/switch/selftest_build_switch.sh`,
`scripts/switch/verify_payload.sh --self-test`,
`luajit tests/switch_ci_workflows_test.lua`,
@@ -151,14 +180,10 @@ or the Switch-related workflow YAML), CI runs:
headlessly (`luajit tests/engine/assets_version_fallback_test.lua`,
`luajit tests/engine/nx_generated_guard_test.lua`,
`luajit tests/engine/nx_yellow_boot_test.lua`).
-2. **Fused NRO build** only on the **canonical** repository
+2. **Fused NRO build** only on the **main** repository
(`bryanthaboi/gen1recomp`), on the self-hosted Mac runner
(`scripts/build_switch.sh --fetch --fused`), and only when the workflow
- head is that repo (same-repo push/PR). **Fork repository** CI never runs
- fused. **Fork → canonical PRs** also skip Switch fused (offline selftest
- still runs) so untrusted head code is not executed on the self-hosted Mac;
- iOS device build eligibility is unchanged. Fused also waits for a successful
- offline selftest before starting on the Mac runner.
+ head is that repo (same-repo push/PR). Fork CI never runs fused. Fork PRs into the main repo also skip Switch fused (offline selftest still runs) so untrusted head code is not executed on the self-hosted Mac; iOS device build eligibility is unchanged. Fused also waits for a successful offline selftest before starting on the Mac runner.
3. On successful PR fused builds, a follow-up workflow posts a PR comment
linking the Actions artifact named `gen1recomp-switch-nro`
(comment tag `switch-build-result`; see
@@ -169,7 +194,7 @@ Unrelated PRs do not burn the self-hosted Mac on Switch packaging.
### Release hard-fail (`.github/workflows/release.yml`)
GitHub Releases always build Switch on the same self-hosted Mac runner as the
-other platforms — this is a **hard gate** (no `continue-on-error`):
+other platforms. This is a **hard gate** (no `continue-on-error`):
```sh
scripts/build_switch.sh --fetch --fused --version ""
@@ -181,9 +206,26 @@ A Switch packaging failure fails the entire release job. The release asset is
### Runner provisioning
-The self-hosted Mac runner must have **native switch-tools** (`nacptool` /
-`elf2nro`) **and/or Docker** available. CI and release do not silently run
-`dkp-pacman -S`; keep the runner image/host provisioned per this guide.
+The self-hosted Mac runner **must** have **DEVKITPRO** installed and exported.
+`--fused` preflight fails early with setup steps if it is missing.
+
+**One-time setup on the runner** (if not already present):
+
+```sh
+# devkitPro pacman installer from https://devkitpro.org/wiki/devkitPro_pacman
+sudo dkp-pacman -S switch-dev
+export DEVKITPRO=/opt/devkitpro
+export PATH="$DEVKITPRO/tools/bin:$PATH"
+
+# OTA launcher: pick one
+bash scripts/switch/install_devkitpro_deps.sh # native
+# or ensure Docker is installed (same pin as fused builds)
+```
+
+CI and release still run `scripts/build_switch.sh --fetch --fused`. Preflight
+requires DEVKITPRO and either native OTA packages or Docker. Without all of
+that, the job fails with the setup steps above. Scripts never auto-run
+`dkp-pacman -S` during CI.
---
@@ -194,11 +236,9 @@ These scripts and this guide do **not**:
- Push files to the console (no automated MTP / FTP / SD scripting)
- Bundle or download any Pokémon ROM
- Install `dkp-pacman` / `switch-dev` for you
-- Provide `nxlink` / netloader deploy (deferred — see [switch-transfer.md](switch-transfer.md))
-- Validate **Applet Mode** — use title override (hold **R**) for full memory
+- Provide `nxlink` / netloader deploy (deferred; see [switch-transfer.md](switch-transfer.md))
+- Validate **Applet Mode**. Use title override (hold **R**) for full memory
Player install steps: [switch-install.md](switch-install.md).
Manual transfer (MTP / SD / FTP, macOS / Linux / Windows):
-[switch-transfer.md](switch-transfer.md).
-Hardware depth and evidence: [switch-development.md](switch-development.md),
-[switch-hardware-evidence.md](switch-hardware-evidence.md).
+[switch-transfer.md](switch-transfer.md).
diff --git a/docs/switch-development.md b/docs/switch-development.md
deleted file mode 100644
index 8e00e072..00000000
--- a/docs/switch-development.md
+++ /dev/null
@@ -1,528 +0,0 @@
-# Nintendo Switch development (love-nx)
-
-> Fused NRO support for issue [#531](https://github.com/bryanthaboi/gen1recomp/issues/531).
-> Releases ship `gen1recomp-*-switch.zip` (SD-ready tree). Console copy is
-> extract/merge at microSD root; title override required. See
-> [Known limitations](#known-limitations-read-before-reviewing).
-
-**Canonical install / build / transfer docs** (start here unless you need hardware depth):
-
-- Players → [switch-install.md](switch-install.md)
-- Builders → [switch-build.md](switch-build.md) (`scripts/build_switch.sh --fetch` downloads the pinned love-nx pair)
-- Transfer (MTP / SD / FTP on macOS, Linux, Windows) → [switch-transfer.md](switch-transfer.md)
-
-This document covers what landed, known limitations, how hardware was tested,
-vendor layout, build/deploy, and the contributor transfer loop (detail lives in
-the transfer runbook).
-
-## Acknowledgments
-
-- **Port / love-nx packaging:** [andrewqsantos](https://github.com/andrewqsantos)
-- **Community hardware testing** (Switch V1 / Erista boot): [booshankles](https://github.com/booshankles)
-- **Method guidance:** [Dusklight Switch port](https://github.com/HayatoG/dusklight/tree/main/platforms/switch) / love-nx
-- **Upstream project:** [bryanthaboi](https://github.com/bryanthaboi) / Gen1Recomp
-
-## Status
-
-| Area | State |
-| ---- | ----- |
-| Feature | **Available** — playable fused NRO path (issue #531) |
-| Runtime | Pinned love-nx **`11.5-nx1`** |
-| Product artifact | Releases: SD-ready `gen1recomp-*-switch.zip`; local/PR: fused `.nro`; loose `nro`+`game.love` for iteration |
-| Hardware | **OLED** validated (author, title override); **V1 / Erista** boot confirmed (community). Lite, docked soak, and Pro Controller matrices welcome |
-| Deploy / install | Releases publish SD-ready zip; **extract/merge at microSD root** (MTP / SD / FTP — [switch-transfer.md](switch-transfer.md)); no `nxlink` path yet |
-| Contributor transfer | Documented for **macOS, Linux, and Windows**; OpenMTP on Mac is one example, not the only contract |
-| Network features on NX | Self-update / remote mod download **disabled** (`networkValidated == false`) |
-| Community help | Welcome — especially HOS / love-nx packaging and broader hardware coverage |
-
-### What landed
-
-- Detect `NX` via `src/core/Platform.lua` without reusing Android flags
-- Writable ROM inbox under `getSaveDirectory()/imports/` + per-tab “Scan again” (SHA-1 match for the open game)
-- Joy-Con / gamepad mapping shared by launcher and gameplay (Nintendo A/B UX on NX)
-- Launcher L/R tab switch; gameplay L/R game-speed cycle; Select+face display chords
-- Focus loss / joystick reconnect recovery; opt-in `switch-debug.txt` diagnostics
-- Loose assemble + fused NRO build scripts (`scripts/build_switch.sh`, `scripts/switch/*`)
-- Payload gates so ROM / generated cache / saves never enter `game.love`
-- Community mod zip inbox at `imports/mods/` (rescan installs; FIND MODS stays network-gated)
-- Raw `.sav` inbox at `imports/saves/{red,blue,yellow}/` (**Import save** rescan) + export pull path `exports/{red,blue,yellow}/` (MTP hint; no openURL)
-- Hardware evidence for Phase 0 probe, ROM import, naming A/B, save/suspend, fused NRO — see `docs/switch-hardware-evidence.md`
-- Path-gated CI selftest + canonical fused PR artifact; release Switch hard-fail
-- Save editor pad/touch input (virtual cursor, A click, B close) — see `tools/save-editor/README.md`
-- Dynamic display size on NX only: handheld **1280×720**, docked/TV **1920×1080** (`src/core/NxDisplay.lua` + resizable conf so love-nx SDL can follow dock/undock at runtime)
-
-### Known gaps / welcome contributions
-
-- Docked vs handheld soak (≥30 min) and Lite coverage — resolution switch is implemented; long soak still welcome
-- Switch Lite and fuller Pro Controller / third-party pad matrices
-- Applet Mode remains unsupported by design (title override required)
-- `nxlink` / netloader contrib fast-loop (deferred — see [switch-transfer.md](switch-transfer.md))
-
-Transfer runbooks for Linux/Windows (and SD/FTP alternatives) are in
-[switch-transfer.md](switch-transfer.md). Community mod zip install OLED smoke
-is **pass** — see NXMOD-12 in [switch-hardware-evidence.md](switch-hardware-evidence.md).
-
-## Design references (Dusklight)
-
-This work borrowed method — not the native stack — from the [Dusklight Switch port](https://github.com/HayatoG/dusklight/tree/main/platforms/switch), especially [`LESSONS_AND_REUSE.md`](https://github.com/HayatoG/dusklight/blob/main/platforms/switch/LESSONS_AND_REUSE.md):
-
-| Dusklight lesson | How Gen1Recomp applied it |
-| ---------------- | ------------------------- |
-| Emulators hide Tegra failures | Gate milestones on **real OLED hardware**, not Ryujinx/Yuzu alone |
-| Prove the lower layer first | `tools/switch-probe` before full launcher |
-| Know which binary ran | Embedded `build-info.json` (commit / love-nx tag) |
-| Cap continuous logs | Opt-in diagnostics, ≤1 Hz flush; Lua error log rotation |
-| Crash symbolization needs the exact ELF | Keep pinned `love.elf` with the NRO under test |
-| Full memory matters | Title override; Applet Mode is not the validation path |
-| Do not treat SD FS like desktop POSIX | Lua stays on `love.filesystem`; inbox + MTP for user files |
-| Isolate platform code | Capability module instead of Android flag overload |
-| NVK / WSI / `audren` stacks | **Not** copied — love-nx already supplies video/audio/input/FS |
-
-The packaging goal matches Dusklight’s **single self-contained `.nro`**; contributor transfer stays multi-host (not Mac-only).
-
-## Known limitations (read before reviewing)
-
-1. **Transfer is manual and multi-method.** Runtime only needs files under the LÖVE save directory / NRO install folder. Use MTP, direct SD, or FTP per [switch-transfer.md](switch-transfer.md). macOS + OpenMTP is a documented example for OLED evidence — not “Switch requires a Mac.”
-2. **Deploy is manual.** There is no automated push to the console and no `nxlink` path yet. Operators build locally, transfer files, then title-override launch.
-3. **Hardware coverage.** Author P0/P1 pass rows were recorded on one Switch OLED; Switch V1 boot was confirmed independently. Treat Lite, docked soak, and other hosts as unknown until someone re-runs the checklist.
-4. **No ROM/save/mod zip bytes in git.** Legal dumps and third-party mods stay on the console (or local untracked folders).
-5. **AppleDouble sidecars** (`._*`) from some MTP clients can break zip/ROM/`.sav` scans — the launcher skips hidden `.*` names (including `._*.sav`); still prefer clean copies.
-
-## How we tested
-
-| Layer | What | Where |
-| ----- | ---- | ----- |
-| Unit / headless | Platform NX flags, RomImporter inbox, dual-path input, mod zip inbox, save `.sav` inbox, display chords, payload/self-tests | `tests/*`, `scripts/test.sh` |
-| Switch CI / packaging | Path-gated offline selftest (`selftest_build_switch.sh`, `verify_payload.sh --self-test`, `switch_ci_workflows_test.lua`); canonical fused PR artifact | `.github/workflows/ci.yml`, [switch-build.md](switch-build.md) § CI and release |
-| Probe on hardware | `getOS()==NX`, 1280×720, save path, Joy-Con events | `tools/switch-probe` → OLED |
-| Integration on hardware | MTP inbox ROM import, Play Red/Blue, naming A/B, quit/reopen save, suspend×10, reboot, fused NRO alone + NRO-only update | `docs/switch-hardware-evidence.md` |
-| Community hardware | Switch V1 / Erista boot with prebuilt NRO | [booshankles](https://github.com/booshankles) — see evidence log |
-| Known gaps | Docked soak, ≥30 min long-play, Lite, automated/`nxlink` deploy | Matrix deferred / absent rows |
-
-Operator evidence must stay in `docs/switch-hardware-evidence.md`. **Do not invent passes** for hardware not run.
-
-## love-nx 11.5-nx1 (pinned)
-
-**Tag:** [11.5-nx1](https://github.com/retronx-team/love-nx/releases/tag/11.5-nx1)
-
-**Local layout (not committed):**
-
-```text
-.bazinga/love-nx/11.5-nx1/
-├── love.nro # homebrew launcher binary (loose mode: copied to gen1recomp.nro)
-└── love.elf # required for fused NRO builds (devkitPro nacptool/elf2nro)
-```
-
-**Manifest:** `scripts/switch/love-nx-11.5-nx1.sha256` lists expected artifact names and SHA-256 checksums. Checksums are filled when binaries are fetched (`TBD_*` placeholders until then).
-
-### Fetch instructions
-
-Preferred (automated checksum verify):
-
-```bash
-scripts/build_switch.sh --fetch
-```
-
-That downloads pinned `love.nro` + `love.elf` into `.bazinga/love-nx/11.5-nx1/`
-and checks them against `scripts/switch/love-nx-11.5-nx1.sha256`. See
-[switch-build.md](switch-build.md) for the full mode glossary.
-
-Manual fallback:
-
-1. Open the [11.5-nx1 release](https://github.com/retronx-team/love-nx/releases/tag/11.5-nx1) and download `love.nro` and `love.elf`.
-2. Create the directory: `mkdir -p .bazinga/love-nx/11.5-nx1`
-3. Move both files into that directory.
-4. Confirm checksums match the manifest:
-
- ```bash
- shasum -a 256 .bazinga/love-nx/11.5-nx1/love.nro \
- .bazinga/love-nx/11.5-nx1/love.elf
- ```
-
-**Never commit** love-nx binaries, ROM dumps, or generated cache into git. The repo `.gitignore` excludes `.bazinga/` (vendor cache) and `/dist/` (build output).
-
-## Loose-mode dist layout
-
-Development builds place `gen1recomp.nro` and `game.love` side by side:
-
-```text
-dist/switch/loose/
-├── gen1recomp.nro
-└── game.love
-```
-
-Assemble with:
-
-```bash
-scripts/build_switch.sh --loose
-```
-
-(See `scripts/switch/assemble_loose.sh` for the underlying copy + checksum step.)
-
-## Transfer & deploy (current contributor loop)
-
-Detail for **macOS / Linux / Windows** and **MTP / SD / FTP** lives in
-[switch-transfer.md](switch-transfer.md). Summary:
-
-| Layer | Intent |
-| ----- | ------ |
-| **Runtime / players** | Extract the release zip at microSD root (`switch/gen1recomp/`) and land ROMs/mods under the save-dir inboxes. The game does not hard-depend on OpenMTP or macOS. |
-| **Contributor loop** | Manual copy via MTP (DBI responder), direct SD (Hekate UMS / reader), or FTP. Fully manual — no CI deploy, no `nxlink` yet. |
-
-The Mac + OpenMTP steps that remain below are the **OLED evidence reproduction** path; prefer the transfer runbook for day-to-day contrib on other hosts.
-
-**Still avoided for routine evidence** (keeps SD handling honest):
-
-- Treating `nxlink` / netloader as the release deploy story (deferred)
-- DBI `MicroSD install` / `NAND install` / NSP-style virtual folders for the `.love`/`.nro` pair
-
-If MTP fails: check cable, USB port, DBI state, and that only one MTP client holds the device — then retry or switch to SD/FTP. Do not silently rewrite evidence using an untested path and claim parity with recorded SHA-256 round-trips.
-
-### Manual deploy checklist (today)
-
-1. Build on the contributor host (`scripts/build_switch.sh --loose` or fused).
-2. Close Gen1Recomp on the Switch; open DBI → `Run MTP responder`.
-3. Copy artifacts with your MTP client into `1: SD Card/switch/gen1recomp/` (and ROMs/mods into the save-dir inboxes when needed).
-4. Wait for the transfer queue; refresh; optionally round-trip SHA-256 on first artifacts of a type.
-5. Exit MTP; launch via **title override** (hold **R** on a title → hbmenu, not Applet Mode).
-
-## OpenMTP + DBI transfer (loose build, Mac evidence example)
-
-Full multi-OS / multi-method steps: [switch-transfer.md](switch-transfer.md).
-The numbered Mac loop below reproduces the OLED evidence path.
-
-### On the Switch
-
-1. Close Gen1Recomp if it is running.
-2. Open **DBI** from hbmenu.
-3. Select **`Run MTP responder`** (DBI documents `X` on the main screen).
-4. Keep DBI on that screen for the entire transfer.
-5. Connect the Switch to the Mac with a USB-C data cable.
-
-### On the Mac
-
-1. Close any other MTP clients.
-2. Open **OpenMTP** and select the DBI device.
-3. In the remote pane, open **`1: SD Card`**.
-4. Navigate to **`switch/`** and create **`gen1recomp/`** if needed.
-5. Enter **`1: SD Card/switch/gen1recomp/`**.
-6. Drag from the local pane:
-
- ```text
- dist/switch/loose/gen1recomp.nro
- dist/switch/loose/game.love
- ```
-
-7. Wait for the OpenMTP queue to finish completely.
-8. Refresh the remote listing and confirm file sizes match the local files.
-9. On the Switch, exit MTP responder normally in DBI before launching the app.
-
-Expected layout on SD:
-
-```text
-1: SD Card/
-└── switch/
- └── gen1recomp/
- ├── gen1recomp.nro
- └── game.love
-```
-
-## Round-trip SHA-256 verification
-
-For the **first deploy** of each artifact type (loose pair, later fused NRO), verify MTP integrity:
-
-1. **Before send** — record local hashes:
-
- ```bash
- shasum -a 256 dist/switch/loose/gen1recomp.nro \
- dist/switch/loose/game.love
- ```
-
-2. **After send** — in OpenMTP, copy the same files from `1: SD Card/switch/gen1recomp/` back to an empty local folder, e.g. `dist/switch/mtp-roundtrip/`.
-
-3. **Compare** round-trip hashes:
-
- ```bash
- shasum -a 256 dist/switch/mtp-roundtrip/gen1recomp.nro \
- dist/switch/mtp-roundtrip/game.love
- ```
-
-4. Local pre-send and round-trip hashes **must match**. Record results in the test report template below.
-
-Repeat whenever a cable glitch or interrupted transfer is suspected.
-
-## Title override launch (full memory)
-
-Applet Mode is **not** the primary validation path. Use **title override** so hbmenu runs with full memory:
-
-1. Confirm the OpenMTP transfer queue finished.
-2. Exit MTP responder in DBI; disconnect USB if desired.
-3. Hold **`R`** while launching any legitimately installed title.
-4. Keep holding until **hbmenu** appears.
-5. Confirm hbmenu does **not** show **Applet Mode**.
-6. Launch **`gen1recomp`** (or the probe NRO during Phase 0).
-
-Album / applet launches are only useful to document applet-specific limitations; P0/P1 gates use title override.
-
-## Phase 0 hardware checklist
-
-Complete **in order** on OLED hardware. Operator fills evidence fields — leave blank until tested.
-
-| Step | Action | Pass | Evidence / notes |
-| ---- | ------ | ---- | ---------------- |
-| P0-0a | Fetch love-nx 11.5-nx1; record manifest SHA-256 | yes | See `scripts/switch/love-nx-11.5-nx1.sha256` |
-| P0-0b | Build `switch-probe.love` per `tools/switch-probe/README.md` | yes | |
-| P0-0c | Assemble loose probe (`game.love` = probe) to `dist/switch/loose/` | yes | |
-| P0-0d | MTP deploy to `1: SD Card/switch/gen1recomp/`; round-trip SHA-256 | yes | nro `8290ac15…5918f5`; love `9f198637…fa2e34f` |
-| P0-0e | Title override → probe boots; `getOS()` shows `NX` | yes | `getOS()`=`NX`, `love._os`=`NX` |
-| P0-0f | Probe lists 1280×720 (or documented dims), save path, gamepad/touch log | yes | save `sdmc:/switch/gen1recomp/switch-probe`; Joy-Con Y→#3 X→#4 |
-| P0-1a | Replace `game.love` with unpatched Gen1Recomp build | yes | feat/switch-nx inbox build |
-| P0-1b | MTP replace `game.love` only; round-trip SHA-256 | yes | |
-| P0-1c | Title override → launcher reaches import screen | yes | |
-| P0-1d | Joy-Con: can navigate launcher (no touch-only) | yes | Full report: `docs/switch-hardware-evidence.md` |
-
-**Operator:** Andrew **Date:** 2026-08-01 **Console:** Switch OLED only
-**Deploy:** manual Mac + OpenMTP + DBI MTP (not automated)
-**love-nx tag:** 11.5-nx1 **gen1recomp commit:** `df7cea4`
-
-## Phase 0 test report template
-
-Copy this block into your hardware notes or PR evidence. **Do not commit ROM files or ROM hashes of private dumps.**
-
-```markdown
-## Switch Phase 0 — hardware report
-
-- Operator:
-- Date:
-- Console model:
-- Atmosphère / HOS version:
-- gen1recomp commit:
-- love-nx tag: 11.5-nx1
-- love.nro SHA-256 (local):
-- game.love SHA-256 (local, pre-send):
-- MTP round-trip SHA-256 (gen1recomp.nro):
-- MTP round-trip SHA-256 (game.love):
-- Title override used: yes / no
-- Applet Mode observed: yes / no (should be no for P0)
-- Probe getOS():
-- Probe dimensions:
-- Probe save directory shown:
-- Gamepad events logged: yes / no
-- Touch events logged: yes / no
-- Unpatched launcher boot: pass / fail
-- Joy-Con launcher navigation: pass / fail / not tested
-- Notes:
-```
-
-## Fast dev loop (loose mode)
-
-While iterating on Lua/assets:
-
-1. Edit on Mac; run `scripts/test.sh --quick`.
-2. Rebuild `.bazinga/work/game.love` (`scripts/build.sh mac --no-notarize` or project pack step).
-3. Close Gen1Recomp on Switch.
-4. DBI → `Run MTP responder`.
-5. OpenMTP → `1: SD Card/switch/gen1recomp/`.
-6. Replace **only** `game.love`; wait for queue + refresh listing.
-7. Exit MTP responder; launch via title override.
-8. Keep `gen1recomp.nro` unchanged until the love-nx pin changes.
-
-```bash
-scripts/test.sh --quick
-scripts/build.sh mac --no-notarize
-scripts/build_switch.sh --loose
-shasum -a 256 .bazinga/work/game.love
-```
-
-## Controller input mapping (NX)
-
-Measured on Switch OLED (`feat/switch-nx`, love-nx `11.5-nx1`, 1280×720). Both `joystickpressed` and `gamepadpressed` fire for Joy-Con; prefer the gamepad path when `joystick:isGamepad()` is true.
-
-| Path | Control | Mapping |
-| ---- | ------- | ------- |
-| `gamepadpressed` | D-pad / left stick | move |
-| `gamepadpressed` | SDL `a` / `b` on **NX** | swapped via `NX_GAMEPAD_BINDINGS`: physical **A** (east) = GB A confirm, physical **B** (south) = GB B cancel |
-| `gamepadpressed` | SDL `a` / `b` on desktop | identity (SDL south = GB A) |
-| `gamepadpressed` | `start` / `back` | Start / Select (+ / −) |
-| `gamepadpressed` | Right / left shoulder (no Select) | Cycle game speed up / down (same as PC hotkey `1` / speed-down path) |
-| `joystickpressed` (raw) | only if **not** `isGamepad()` | face/menu fallback |
-| `joystickpressed` (raw) | `#1` / `#2` on NX | Nintendo B / A → GB B / A |
-| `joystickpressed` (raw) | `#9` / `#10` | Select / Start (− / +) |
-
-**Nintendo UX on Switch:** physical A confirms, physical B cancels (explicit NX remap of SDL face labels).
-
-**Launcher extras** (`RomImporter`): physical **A** clicks at the virtual cursor; **L** / **R** switch tabs; **Start** / **Select** start Play when a ROM is ready (else open Choose ROM). D-pad / left stick move the virtual cursor.
-
-**Dual-path rule:** love-nx emits both `gamepadpressed` and `joystickpressed` for Joy-Con. When `joystick:isGamepad()` is true, Input and RomImporter **ignore raw** face/menu so NamingScreen does not see A+B in one frame. `NamingScreen` also prefers A over B if both edges still fire.
-
-Implementation: `src/core/GamepadMap.lua` (`NX_RAW_*`, `ignoreRawForJoystick`, `displayChordDigit`), `src/core/Game.lua` (shoulder speed), `src/import/RomImporter.lua` (launcher tabs). Launcher and gameplay share the same converter.
-
-## ROM inbox (NX)
-
-Legal dumps land in a shared MTP inbox; **Scan again** is tab-scoped:
-
-| Item | Value |
-| ---- | ----- |
-| Save-relative path | `imports/` (also accepts loose `.gb`/`.gbc` at the save-dir root) |
-| MTP destination | `1: SD Card//imports/` (see launcher notice for the live `getSaveDirectory()` path) |
-| Candidates | `*.gb` / `*.gbc` (hidden `.*` AppleDouble names skipped) |
-| Rescan | Game tab → **Scan again** — imports only the dump whose SHA-1 matches that tab (`GameVersion.forSha1`). Other known dumps stay for their own tabs |
-| Already ready | Same SHA already imported → “No new ROM found.” |
-
-Players may drop Red, Blue, and Yellow into the same folder. Opening Yellow and pressing **Scan again** must not start a Red import.
-
-## Mod zip inbox (NX)
-
-Community mods install from a **separate** MTP inbox (not mixed into the ROM `imports/` scan):
-
-| Item | Value |
-| ---- | ----- |
-| Save-relative path | `imports/mods/` |
-| MTP destination | `1: SD Card//imports/mods/` (see launcher notice for the live `getSaveDirectory()` path) |
-| Candidates | `*.zip` only |
-| Rescan | MODS tab → **Scan again** (installs each zip via `LauncherMods.installZip`; source zips are retained on success and failure) |
-| FIND MODS | Remains network-gated / hidden on NX (`networkValidated == false`) |
-
-Do **not** commit third-party mod zip bytes into git. Drop the zip over MTP, rescan, enable in MODS, then Play.
-
-**MTP tip (esp. macOS clients):** OpenMTP/Finder often creates AppleDouble sidecars named `._Something.zip` / `._cart.gb` / `._foo.sav`. Those are not real archives, ROMs, or saves — the launcher ignores hidden `.*` names under `imports/`, `imports/mods/`, and `imports/saves//`. If install still fails with “could not be opened” / “not a zip file”, delete any `._*` under the inbox and confirm the real zip starts with the `PK` magic (re-copy the release asset if unsure). This is a host-side annoyance of the current manual MTP loop, not something players should need forever.
-
-Drop any community release `.zip` into `imports/mods/`, rescan, enable.
-Player-facing install steps: [switch-install.md](switch-install.md#community-mods).
-Mods own their OPTIONS / rebinds — do not duplicate third-party control tables here.
-
-## Save `.sav` inbox (NX)
-
-Raw Gen1 battery images use a **separate** MTP inbox (not mixed into ROM `imports/` or mod `imports/mods/`):
-
-| Item | Value |
-| ---- | ----- |
-| Save-relative path | `imports/saves/red/`, `imports/saves/blue/`, `imports/saves/yellow/` |
-| MTP destination | `1: SD Card//imports/saves//` (see launcher notice for the live `getSaveDirectory()` path) |
-| Candidates | non-hidden `*.sav` only in **that game’s** folder |
-| Rescan | SAVE FILES → **Import save** on the matching game tab (scans only that folder) |
-| After success | Retire to `*.sav.imported` + append content hash to `imports/saves//.imported-sha1` |
-| Exports | **Export save** writes under `exports//gen1recomp--.sav`; NX shows an MTP path notice (no `openURL`) |
-
-Do **not** commit `.sav` bytes into git. Drop the file into the matching game folder over MTP, press **Import save** on that tab, then play. Pull exports from `exports//`.
-
-**MTP tip:** the same AppleDouble `._*.sav` rule applies — see the mod inbox tip above.
-
-## Joy-Con display chords (Select + face)
-
-PC digit hotkeys for COLORS / TILT / GBC FX / pipelines have Joy-Con equivalents. Hold **Select** (`back` / −) and press a face/shoulder button; the engine runs the same path as `Game:keypressed` for that digit (including `writeOptions` / Pipelines parity).
-
-| Chord (Nintendo UX) | Engine key | Stock engine effect |
-| ------------------- | ---------- | ------------------- |
-| Select + **A** | `2` | COLORS cycle |
-| Select + **B** | `3` | TILT / perspective |
-| Select + **Y** | `5` | GBC FX |
-| Select + **X** | `6` | Mod pipeline hotkey (if registered) |
-| Select + **L** (left shoulder) | `7` | Mod pipeline hotkey (if registered) |
-
-Keys `2` / `3` / `4` / `5` are claimed by the engine before mod pipeline hotkeys run, so a community mod cannot rebind those digits through `Pipelines.hotkey`. Mods that need their own controls should use OPTIONS rows or unclaimed hotkeys.
-
-Without Select held, face buttons keep normal GB A/B gameplay mapping (no accidental color/tilt cycles). The **Options** menu remains available for the same settings — chords are optional shortcuts, not the only path.
-
-On NX, A/B chords resolve through the Nintendo UX face remap so physical **A** → key `2` and physical **B** → key `3` match this table.
-
-**OPTIONS → PERFORMANCE** clamps the port’s own extras (TILT / GBC FX / survey ZOOM) and can cap FPS — useful on weaker handheld budgets. Details: [new-features.md — Performance tier](new-features.md#performance-tier-low-end-devices).
-
-Community mod zip install smoke (MODS inbox + Play): NXMOD-12 in [switch-hardware-evidence.md](switch-hardware-evidence.md).
-
-**Opt-in diagnostics:** create an empty `switch-debug.txt` in the save directory; events flush to `switch.log` at ≤1 Hz with build identity (no ROM/save bytes).
-
-**NX asset probe (always on Play):** every Switch Play writes `nx-asset-probe.log` in the save directory (`pokemon-love2d/`). It lists whether `assets/generated/…` vs `yellow|blue/assets/generated/…` exist, what `Assets.resolve` returns, and whether `newImage` / `newImageData` open — for Yellow/Blue blank-sprite triage. No ROM bytes.
-
-**Blue/Yellow cache overlay (NX):** fused love-nx cannot reliably mount `yellow|blue/assets/generated` onto the un-prefixed path, so `src/core/NxAssetOverlay.lua` wraps EVERY read-side love API that accepts a filesystem path (`filesystem.read/load/lines/newFileData/getInfo`, `graphics.newImage/newFont`, `image.newImageData`, `audio.newSource`, `sound.newSoundData`, `font.newFontData`) once at boot — only when `Platform.isNX()`. Covering the whole read surface (not just the loaders the boot needs today) keeps future states and mods inside the fallback automatically; write-side functions stay stock. Core code must NOT call love loaders on literal `assets/generated` paths (enforced by `tests/engine/nx_generated_guard_test.lua`); the chip-audio worker is a separate Lua state and gets the prefix explicitly via `audio.programPrefix` from `ChipAudio.slimAudio`.
-
-**Hardware re-test:** T16 **pass** @ `2699c9a` (naming A=confirm / B=cancel). T19 **pass** (quit/reopen, suspend×10, reboot) — operator 2026-08-01.
-
-**Suspend/resume audio:** after resume, chip music is stopped to avoid duplicate streams; confirm on hardware during P0-09/10 (T19).
-
-## Lua error log (save directory)
-
-On any uncaught Lua error, Gen1Recomp appends a redacted trace to `lua-error.log` in the LÖVE save directory (`love.filesystem.getSaveDirectory()`). The on-screen error overlay includes a hint pointing at that file. Logs rotate to `lua-error.log.1` when the active file exceeds 32 KiB. ROM/save bytes and non-printable data are stripped — never commit or share logs that might contain private paths without reviewing them first.
-
-## Native crash triage (love-nx / Atmosphère)
-
-love-nx native faults land under the console’s `crash_reports/` folder on SD (reachable via the same manual MTP workflow used for game deploys).
-
-1. **Collect** — DBI → `Run MTP responder`; copy `sdmc:/crash_reports/*.bin` (or the dated subfolder) to the contributor host. Prefer keeping the microSD in-console for routine pulls.
-2. **Redact** — delete any attached screenshots or notes that mention ROM filenames, save paths, or private hashes before sharing logs publicly.
-3. **Symbolize** — use the **pinned** `love.elf` from `.bazinga/love-nx/11.5-nx1/` that matches `build-info.json` / `scripts/switch/love-nx-11.5-nx1.sha256`. Never use a “latest” download.
-
- ```bash
- # Example: aarch64-none-elf-addr2line from devkitPro
- aarch64-none-elf-addr2line -e .bazinga/love-nx/11.5-nx1/love.elf -f -C 0xADDRESS_FROM_CRASH_REPORT
- ```
-
-4. **Correlate** — compare `gitCommit` / `loveNxTag` from embedded `build-info.json` with the operator’s hardware notes.
-
-If `addr2line` cannot resolve an address, archive the crash `.bin` with the exact `love.elf` SHA-256 used for the build — addresses are only meaningful against that ELF.
-
-## P0 / P1 hardware matrix (ADR §9)
-
-Operator evidence lives in `docs/switch-hardware-evidence.md`. **Do not invent passes** for rows that require hardware not yet run.
-
-| ID | Requirement | Status | Evidence |
-| -- | ----------- | ------ | -------- |
-| P0-0a–f | love-nx pin, probe, MTP, title override | **pass** | Phase 0 checklist above; T4 |
-| P0-1a–d | Unpatched launcher boot + Joy-Con nav | **pass** | T4 / `docs/switch-hardware-evidence.md` |
-| P0-02 | MTP inbox import path shown | **pass** | T12 |
-| P0-03 | Rescan imports ROM | **pass** | T12 |
-| P0-04 | Canonical hash routes version | **pass** | T12 |
-| P0-05 | Source dump retained in inbox | **pass** | T12 |
-| P0-06 | Play reaches game after import | **pass** | T12 |
-| P0-07 | Joy-Con launcher navigation | **pass** | T16 @ `2699c9a` |
-| P0-08 | Joy-Con gameplay (incl. naming A/B) | **pass** | T16 @ `2699c9a` |
-| P0-09 | Save survives quit + reopen | **pass** | T19 |
-| P0-10 | ≥10 suspend cycles, no stuck input/dup audio | **pass** | T19 (operator 2026-08-01) |
-| P0-12 | Fused NRO boots without adjacent `game.love` | **pass** | T24 — `docs/switch-hardware-evidence.md` |
-| P0-14 | Fused NRO MTP round-trip SHA-256 | **pass** | T24 — first artifact `b019e2e8…` @ `6fb5602` (redeploy after Blue fix) |
-| P0-15 | Replace NRO only; saves persist | **pass** | T24 — operator NRO-only update keeps saves |
-| P1-01 | Docked vs handheld spot-check | **deferred** | Code: `NxDisplay` 720p↔1080p; OLED dock soak not recorded yet |
-| P1-02 | Applet Mode documented unsupported | **pass** | Title override required; Album path not validated |
-| P1-03 | Long-play soak (≥30 min) | **deferred** | No soak session recorded |
-| P1-04 | Reboot persistence | **pass** | T19 |
-| P1-05 | Audio resume after suspend | **pass** | T19 (no dup audio reported) |
-| — | Switch V1 / Erista boot | **pass** (boot) | Community — [booshankles](https://github.com/booshankles); see evidence log |
-| — | Switch Lite / docked soak | **untested** / **deferred** | Welcome contributions |
-| — | Automated / `nxlink` deploy | **absent** | Manual MTP / SD / FTP only (AD-009) |
-| — | Multi-OS transfer runbooks | **pass** | [switch-transfer.md](switch-transfer.md) |
-| — | Community mod zip OLED smoke (NXMOD-12) | **pass** | `docs/switch-hardware-evidence.md` |
-
-## Review guidance
-
-Maintainers may review as one PR or split later. Suggested slices (optional):
-
-Each slice should declare: **no ROM/save bytes committed**, **love-nx pin with manifest checksums**, **hardware-tested rows listed with linked evidence**, **Applet Mode unsupported**, **network/updater disabled on NX**, **deploy still manual** (MTP / SD / FTP; no nxlink yet), **OpenMTP is one example not the sole contract**.
-
-### Slice 1 — Platform + import (`platform/import`)
-
-- `src/core/Platform.lua`, `conf.lua` NX branch
-- `src/import/RomImporter.lua` (NX flags, inbox, scan, shell/updater gates)
-- Tests: `tests/engine/platform_nx_*`, `tests/engine/rom_importer_nx_*` (ROM-free T2)
-- Docs: inbox/MTP import sections only
-
-### Slice 2 — Input + lifecycle (`input/lifecycle`)
-
-- `src/core/GamepadMap.lua`, `Input.lua`, `main.lua` focus/joystick hooks
-- `src/debug/SwitchDiagnostics.lua` (opt-in probe + error log)
-- Tests: input/diagnostics suites
-- Docs: controller mapping, suspend/audio notes
-
-### Slice 3 — Build + docs (`build/docs`)
-
-- `scripts/pack_love.sh`, `scripts/build_switch.sh`, `scripts/switch/*`
-- `assets/switch/icon.jpg`, `docs/switch-development.md`, hardware evidence templates
-- Gates: `pack_love.sh --dry-run`, `verify_payload.sh --self-test`, fused build script (devkitPro host)
-
-**Pre-merge checklist:**
-
-- [ ] Manifest `scripts/switch/love-nx-11.5-nx1.sha256` filled; binaries not in git
-- [ ] `verify_payload.sh` rejects generated cache / ROM / `.sav` / `.bak`
-- [ ] P0 matrix rows marked pass only with linked hardware evidence
-- [x] Fused NRO P0-12/14/15 pass with T24 evidence (`docs/switch-hardware-evidence.md`)
-- [ ] Updater / remote mod download hidden on NX (`networkValidated == false`)
-
diff --git a/docs/switch-hardware-evidence.md b/docs/switch-hardware-evidence.md
deleted file mode 100644
index 12e93c04..00000000
--- a/docs/switch-hardware-evidence.md
+++ /dev/null
@@ -1,187 +0,0 @@
-# Switch hardware evidence (Phase 0 + import + input)
-
-> **Hardware evidence log.** Author passes below were recorded on **one
-> Nintendo Switch OLED** with a **manual** Mac → DBI MTP deploy loop. A
-> separate community row records Switch V1 / Erista boot. These rows do
-> **not** claim Lite, docked soak, or automated install. See
-> `docs/switch-development.md` for status and limitations.
-
-**love-nx:** `11.5-nx1`
-**Author console:** Switch OLED
-**Deploy method (author):** manual OpenMTP + DBI `Run MTP responder` (no CI / no nxlink)
-**Operator (author rows):** Andrew ([andrewqsantos](https://github.com/andrewqsantos))
-**Date (author rows):** 2026-08-01
-
-Do **not** commit ROM dumps or private dump hashes. Do **not** mark a row **pass** without hardware notes for that row.
-
----
-
-## Community — Switch V1 / Erista boot — pass (boot)
-
-| Field | Value |
-| ----- | ----- |
-| Console | Nintendo Switch V1 (Erista) |
-| Check | Prebuilt fused NRO boots under title override |
-| Tester | [booshankles](https://github.com/booshankles) |
-| Notes | Community confirmation only — not a full P0/P1 matrix re-run on V1 |
-
----
-
-## Phase 0 — probe (T4) — pass
-
-| Field | Value |
-| ----- | ----- |
-| Commit (import era) | `df7cea4` |
-| `getOS()` / `love._os` | `NX` |
-| Dimensions | 1280×720 |
-| Save (probe) | `sdmc:/switch/gen1recomp/switch-probe` |
-| Joy-Con | `joystickpressed` + `gamepadpressed` (Y→`#3`, X→`#4`) |
-
-| Artifact | SHA-256 |
-| -------- | ------- |
-| `gen1recomp.nro` | `8290ac153d4c630e48c9b26ef9123f5204ed8ee0cef3042511707b5b645918f5` |
-
----
-
-## T12 — Red import + Play — pass
-
-Inbox MTP → “Scan again” → Play; Joy-Con launcher/gameplay (not touch-only).
-
----
-
-# T16 — Joy-Con launcher + gameplay — pass (naming re-verify)
-
-### Round 1 @ `7504753` — partial
-
-| Check | Result |
-| ----- | ------ |
-| Launcher / overworld (Joy-Con only) | **pass** |
-| Naming player/rival | **fail** (dual-path a+b; see below) |
-| Touch required | **no** |
-| `game.love` SHA-256 | `bd3a35461bf453c1f0465a5a289421aef3b5c72d3bf1f8d76e86231256829e0e` |
-
-### Naming failure (root cause) — fixed in `efd81d8` + `2699c9a`
-
-- love-nx fires **`gamepadpressed` + `joystickpressed` on the same physical press**.
-- `NamingScreen` tested `wasPressed("b")` before `"a"` → if both true in one frame, always deletes.
-- Dual-path fix: ignore raw when `isGamepad()` (`efd81d8`).
-- SDL-only UX then had physical B confirm / A erase; NX face remap (`2699c9a`) restores Nintendo A=confirm / B=cancel.
-
-### Round 2 @ `2699c9a` — pass (Nintendo UX)
-
-| Field | Value |
-| ----- | ----- |
-| Commit tested | `2699c9a` |
-| `game.love` SHA-256 | `a208b21e1f30b00e2e8c6fa6efe14f0e06d1db0ae1e50b810b16d9fb852926bc` |
-| Touch required | **no** |
-
-| Check | Result |
-| ----- | ------ |
-| Naming — player | **pass** — physical **A** confirms letter, **B** cancels/erases |
-| Naming — rival | **pass** (same) |
-| Launcher / overworld (prior round) | **pass** (unchanged mapping for d-pad/stick) |
-
-T16 hardware gate: **closed**.
-
----
-
-## T19 — save / suspend — pass
-
-| Check | Result |
-| ----- | ------ |
-| Save in-game → full quit → title-override reopen → load save | **pass** (@ `7504753` / retained) |
-| Suspend/resume ×10 (launcher / gameplay / mixed) | **pass** (operator 2026-08-01) |
-| Full console reboot persistence | **pass** (operator 2026-08-01) |
-
-T19 hardware gate: **closed**. No stuck input, duplicate audio, or crash reported.
-
----
-
-## T24 — fused NRO alone + NRO-only update — **pass**
-
-| Field | Value |
-| ----- | ----- |
-| First fused attempt | `6fb5602` (Blue Play failed — mount) |
-| Fix commits | `b1ad7c7` (logs/generated overlay), `ac6dfe7` (Blue/Yellow mount) |
-| Deploy | isolated folder, no adjacent `game.love` |
-| Boot fused | **pass** |
-| ROM import | **pass** |
-| Play **Red** | **pass** |
-| Play **Blue** (after `ac6dfe7`) | **pass** (operator 2026-08-01) |
-| NRO-only replace | **pass** — saves retained; app still boots/plays |
-| Touch required | no |
-
-T24 hardware gate: **closed**.
-
----
-
-## SWBLD — `build_switch.sh --fetch --fused` + install path — **pass**
-
-Operator smoke for the switch-build-pipeline packaging CLI (closes matrix-deferred happy paths from validation).
-
-| Field | Value |
-| ----- | ----- |
-| Command | `scripts/build_switch.sh --fetch --fused --version 0.0.0-test` |
-| Host | macOS + native switch-tools (or Docker fallback if used) |
-| Commit / build-info | `9147a64` (`gitCommit` in build-info) |
-| love-nx | `11.5-nx1` (manifest checksums match) |
-| Artifact | `dist/switch/gen1recomp-0.0.0-test-switch.nro` |
-| NRO SHA-256 | `210efb884a8d27443dc1c64ed8f071b0f862d8d0c9b140ad8185093c4e4027db` |
-| Install doc | `docs/switch-install.md` — at the time of this row: copy NRO under `sdmc:/switch/gen1recomp/` (releases now ship an SD-ready zip; same folder) |
-| Console | Switch OLED |
-| Operator | Andrew |
-| Date | 2026-08-01 |
-
-| Check | Result |
-| ----- | ------ |
-| `--fetch` + `--fused` produce NRO + `.sha256` | **pass** |
-| Copy NRO to SD folder per install doc | **pass** (operator) |
-| Title-override launch / play | treated as prior T24 path; this row records **packaging + deploy to folder** success |
-
-SWBLD packaging smoke: **closed** for Mac fused build + file-to-SD install step.
-
----
-
-## NXMOD-12 — Community mod zip OLED smoke — **pass**
-
-Closed from existing OLED photo evidence on issue
-[#531](https://github.com/bryanthaboi/gen1recomp/issues/531) (operator comment
-with launcher MODS + overworld shots). Photos live on the orphan branch
-[`switch-oled-photos`](https://github.com/andrewqsantos/gen1recomp/tree/switch-oled-photos)
-of the operator fork — **not** committed to this repo. Do **not** commit
-third-party mod `.zip` bytes. Community mods own their OPTIONS / rebinds;
-this entry only proves the MODS inbox + Play path on OLED.
-
-| Field | Value |
-| ----- | ----- |
-| Status | **pass** |
-| gen1recomp commit | evidence era on `feat/switch-nx` (see #531); packaging pin love-nx `11.5-nx1` |
-| love-nx tag | `11.5-nx1` |
-| Console | Switch OLED |
-| Mod | community release `.zip` (not vendored; not named here) |
-| Zip committed to git? | **no** |
-| Photo evidence | [#531 comment](https://github.com/bryanthaboi/gen1recomp/issues/531) — MODS tab + overworld |
-| MODS tab photo | https://raw.githubusercontent.com/andrewqsantos/gen1recomp/switch-oled-photos/IMG_1766.jpg |
-| Overworld photo | https://raw.githubusercontent.com/andrewqsantos/gen1recomp/switch-oled-photos/IMG_1771.jpg |
-| Operator | Andrew |
-| Date | 2026-08-01 |
-
-### Checklist
-
-| Step | Pass / fail / pending | Notes |
-| ---- | --------------------- | ----- |
-| MTP zip into save `imports/mods/` | **pass** | Photo evidence + prior inbox path |
-| MODS → Scan again → mod listed | **pass** | IMG_1766 — community mod installed |
-| Enable mod + Play Red boots without crash | **pass** | Overworld / Pallet / Oak lab photos on #531 |
-| Overworld Select+A → visible colors change | **pass** | Stock COLORS chord path exercised |
-| Overworld Select+B → visible tilt/perspective change | **pass** | Stock TILT chord path exercised (IMG_1771) |
-
-### Evidence notes
-
-```text
-Operator: Andrew
-Date: 2026-08-01
-Commit tested: feat/switch-nx era documented on issue #531
-Pass / fail summary: PASS — MODS zip install + Play on Switch OLED
-Photo branch: andrewqsantos/gen1recomp@switch-oled-photos
-```
diff --git a/docs/switch-install.md b/docs/switch-install.md
index e173dfc2..2a54aa06 100644
--- a/docs/switch-install.md
+++ b/docs/switch-install.md
@@ -2,15 +2,11 @@
Every GitHub Release that includes Switch support ships an SD-ready zip:
`gen1recomp-*-switch.zip`. Extract it at the root of your microSD (install
-**or** update — same steps), launch with **title override**, then import your
+or update, same steps), launch with **title override**, then import your
own legal `.gb` ROM.
> You need a console that can run Switch homebrew (custom firmware / hbmenu).
-> This project does not help you set that up. Tracks issue
-> [#531](https://github.com/bryanthaboi/gen1recomp/issues/531).
-> Hardware: **OLED** validated by the porter; **V1 / Erista** boot confirmed
-> by the community. Lite and other setups welcome more reports.
-> See [switch-development.md](switch-development.md) for limitations.
+> This project does not help you set that up.
Prefer building from source? See [switch-build.md](switch-build.md).
@@ -29,12 +25,18 @@ help from [booshankles](https://github.com/booshankles).
Extract the zip at the **root** of the microSD so you get:
```text
-sdmc:/switch/gen1recomp/gen1recomp.nro
+sdmc:/switch/gen1recomp/gen1recomp.nro # native OTA launcher (hbmenu entry)
+sdmc:/switch/gen1recomp/gen1recomp-game.nro # fused LÖVE game
+sdmc:/switch/gen1recomp/version.txt
sdmc:/switch/gen1recomp/pokemon-love2d/imports/
sdmc:/switch/gen1recomp/pokemon-love2d/imports/mods/
sdmc:/switch/gen1recomp/pokemon-love2d/imports/saves/...
```
+Older single-NRO zips only had `gen1recomp.nro` (the fused game). Current
+releases use the dual-NRO layout above. Open `gen1recomp` in hbmenu (the
+launcher).
+
Merge folders if your OS asks. Any method works: **MTP** (DBI → Run MTP
responder + a client), **direct SD** (Hekate UMS or a card reader), or **FTP**.
Exit MTP / unmount / stop FTP cleanly before launching. Step-by-step for
@@ -42,17 +44,44 @@ macOS, Linux, and Windows: [switch-transfer.md](switch-transfer.md).
### Updating
-Use the **same** extract/merge. It replaces `gen1recomp.nro` (and the small
-help `README.txt` / `INSTALL.txt` files). Saves, imported ROMs, mods, and
-options live under `pokemon-love2d/` — **do not delete that folder** when
-updating, or you will lose progress.
+#### Native OTA launcher (in-console)
+
+Switch OTA runs in a separate **native launcher NRO** (libnx + curl), not the
+LÖVE self-updater (`src/update/Check.lua`). hbmenu opens `gen1recomp.nro`.
+
+When a newer release exists, the launcher downloads the same install zip
+(`gen1recomp-*-switch.zip`), checks SHA-256 against `sha256sums.txt`, replaces
+both `gen1recomp-game.nro` and `gen1recomp.nro` (keeps NACP version in sync
+for hbmenu and Sphaira), then loads the game with `envSetNextLoad`.
+
+If you are up to date or offline, it skips straight to the game with no
+prompt. If an update is available, you get a short prompt styled like the
+in-game launcher: black background, RGB rail, logo, A/B buttons. Saves under
+`pokemon-love2d/` are not touched. See `src/update/SwitchOta.lua` for the
+wire format.
+
+The LÖVE self-updater stays **disabled** on NX (`networkValidated == false`).
+
+**Sphaira forwarder (HOME shortcut):** Sphaira copies name/version/icon into
+the installed forwarder at creation time. After an OTA (or zip) update, the
+`.nro` on the microSD already has the new version, but the HOME shortcut
+keeps the old badge until you **reinstall the forwarder once** in Sphaira
+(Install Forwarder again on `gen1recomp.nro`). Browsing the NRO in Sphaira /
+hbmenu always shows the live file version.
+
+#### Manual zip (fallback)
+
+Use the **same** extract/merge of `gen1recomp-*-switch.zip`. It replaces the
+NROs (and the small help `README.txt` / `INSTALL.txt` files). Saves,
+imported ROMs, mods, and options live under `pokemon-love2d/`. **Do not
+delete that folder** when updating, or you will lose progress.
## 3. Launch with title override
**Applet Mode is not supported** for this game (not enough memory).
1. On the Switch HOME menu, highlight any installed title.
-2. Hold **R** and launch that title — this opens hbmenu with full memory
+2. Hold **R** and launch that title. This opens hbmenu with full memory
(title override).
3. From hbmenu, open `gen1recomp`.
@@ -66,14 +95,14 @@ This project ships **no** game data. On first launch:
(`.gbc`) dump into `switch/gen1recomp/pokemon-love2d/imports/` (the
launcher also shows the live save-dir path). All three can sit in the
same folder.
-2. Use **Scan again** on that game’s tab (Red / Blue / Yellow). Rescan
- matches by ROM SHA-1 for the open tab only — a Red dump never imports
+2. Use **Scan again** on that game's tab (Red / Blue / Yellow). Rescan
+ matches by ROM SHA-1 for the open tab only. A Red dump never imports
from the Yellow tab (and vice versa).
## 5. Import / Export a raw `.sav`
Continue a cart or PC battery save (or pull a slot off-console) via MTP /
-SD / FTP — same transfer methods as ROMs. Paths are **per game**:
+SD / FTP, same transfer methods as ROMs. Paths are **per game**:
| Game | Import inbox | Export folder |
| ---- | ------------ | ------------- |
@@ -81,19 +110,19 @@ SD / FTP — same transfer methods as ROMs. Paths are **per game**:
| Blue | `imports/saves/blue/` | `exports/blue/` |
| Yellow | `imports/saves/yellow/` | `exports/yellow/` |
-(Under the save dir `pokemon-love2d/` — the zip already creates these folders.)
+(Under the save dir `pokemon-love2d/`. The zip already creates these folders.)
-1. Copy a Gen1 `.sav` (32 KB) into that game’s inbox under the save dir
+1. Copy a Gen1 `.sav` (32 KB) into that game's inbox under the save dir
([switch-transfer.md](switch-transfer.md)).
-2. With the game’s ROM already imported, open **that game’s tab** →
+2. With the game's ROM already imported, open **that game's tab** →
**SAVE FILES** → **Import save**. Only that folder is scanned.
3. A successful import retires the file to `*.sav.imported` and records its
content hash so pressing **Import save** again does not clone slots.
Failed imports leave the original `.sav` in place.
4. To pull a slot off the console, use **Export save**, then copy the file
- from that game’s **`exports//`** folder via MTP / SD / FTP.
+ from that game's **`exports//`** folder via MTP / SD / FTP.
-Do not put `.sav` files into git. Prefer clean copies — some MTP clients
+Do not put `.sav` files into git. Prefer clean copies. Some MTP clients
create `._*.sav` AppleDouble sidecars that are not real saves.
## Controls
@@ -130,12 +159,12 @@ create `._*.sav` AppleDouble sidecars that are not real saves.
Mods install from a zip inbox (same transfer methods as ROMs):
1. Copy a release `.zip` into the save-dir **`imports/mods/`** path the
- launcher shows (MTP / SD / FTP — [switch-transfer.md](switch-transfer.md)).
+ launcher shows (MTP / SD / FTP. See [switch-transfer.md](switch-transfer.md)).
2. In the launcher, open **MODS** → **Scan again** → enable the mod →
**Play**.
Remote **FIND MODS** / GitHub download stays **off** on Switch. Do not put
-mod zips into git. Community mods ship their own OPTIONS / rebinds — this port
+mod zips into git. Community mods ship their own OPTIONS / rebinds. This port
does not document third-party control tables.
### Joy-Con shortcuts (Select + face)
@@ -153,12 +182,23 @@ hotkeys (`2`/`3`/`5` are claimed before any mod pipeline hotkey runs).
| Select + **L** | `7` | Mod pipeline hotkey (if a mod registers `7`) |
If the handheld stutters with extras on, try **OPTIONS → PERFORMANCE** →
-`LOW` or `BALANCED`. Full chord notes for contributors:
-[switch-development.md](switch-development.md#joy-con-display-chords-select--face).
+`LOW` or `BALANCED`.
+
+## Limitations
+
+- You need homebrew (custom firmware, hbmenu). This project does not set that
+ up.
+- Launch with title override (hold **R** on a title). Applet Mode (Album) is
+ not supported. The game needs full memory.
+- ROMs, mods, and saves are copied manually via MTP, direct SD, or FTP. There
+ is no automated deploy.
+- Updates use the native OTA launcher only. The LÖVE self-updater and remote
+ **FIND MODS** stay off on Switch.
+- Tested on Switch OLED. Switch V1 / Erista boot confirmed by the community.
+ Other models may work but are less tested.
## Prefer building it yourself?
Building the fused NRO (and SD-ready zip) from source is covered in
[switch-build.md](switch-build.md). Copying artifacts and inbox files
(MTP / SD / FTP on macOS, Linux, Windows): [switch-transfer.md](switch-transfer.md).
-Status, limitations, and how we tested: [switch-development.md](switch-development.md).
diff --git a/docs/switch-transfer.md b/docs/switch-transfer.md
index f7cd153b..1e53de2f 100644
--- a/docs/switch-transfer.md
+++ b/docs/switch-transfer.md
@@ -1,17 +1,16 @@
# Switch file transfer (MTP / SD / FTP)
-Canonical ways to put Gen1Recomp artifacts and inbox files onto a Nintendo
-Switch. **Any method is valid** if the bytes land in the destinations below.
+Ways to put Gen1Recomp artifacts and inbox files onto a Nintendo Switch.
+**Any method is valid** if the bytes land in the destinations below.
-This is the home runbook for contributors on **macOS, Linux, and Windows**.
+This is the runbook for contributors on **macOS, Linux, and Windows**.
Player install (what to download, title override) stays in
[switch-install.md](switch-install.md). Packaging stays in
-[switch-build.md](switch-build.md). Hardware evidence lives in
-[switch-hardware-evidence.md](switch-hardware-evidence.md).
+[switch-build.md](switch-build.md).
> **Not supported yet:** `nxlink` / hbmenu netloader automation. Useful later
-> for a fast contrib rebuild loop; deferred on purpose (AD-009). Do not treat
-> netloader as the release or ROM/mod install path.
+> for a fast contrib rebuild loop; deferred (AD-009). Do not treat netloader as
+> the release or ROM/mod install path.
---
@@ -23,7 +22,7 @@ Player install (what to download, title override) stays in
| Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it |
| ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card//imports/`) |
| Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** |
-| Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow/` then that game’s SAVE FILES → **Import save** |
+| Save `.sav` inbox | Same save dir → `imports/saves/red\|blue\|yellow/` then that game's SAVE FILES → **Import save** |
| Save exports | Same save dir → `exports/red\|blue\|yellow/` (pull after **Export save**; MTP / SD / FTP) |
| Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` |
| Lua error log | `lua-error.log` in the save dir |
@@ -34,7 +33,7 @@ files, or third-party mod zips to git.
---
-## Canonical methods
+## Transfer methods
### 1. MTP (DBI responder + host client)
@@ -47,8 +46,8 @@ before launching.
#### macOS (example: OpenMTP)
-[OpenMTP](https://github.com/ganeshrvel/openmtp) is the loop used for OLED
-hardware evidence — **one contributor example**, not a Mac-only product rule.
+[OpenMTP](https://github.com/ganeshrvel/openmtp) is a documented example for
+macOS, not a Mac-only requirement.
1. Quit other MTP clients.
2. Open OpenMTP → select the DBI device → **`1: SD Card`**.
@@ -60,25 +59,25 @@ hardware evidence — **one contributor example**, not a Mac-only product rule.
5. Wait for the queue; refresh; exit MTP responder; title-override launch.
macOS clients often create AppleDouble sidecars (`._Something.zip`,
-`._cart.gb`, `._foo.sav`). Those are not real archives or saves — the
+`._cart.gb`, `._foo.sav`). Those are not real archives or saves. The
launcher skips hidden `.*` names. Delete `._*` junk if a zip/ROM/`.sav`
fails to open.
#### Linux
1. Install desktop MTP support if needed (e.g. `gvfs-mtp` on GNOME/GTK
- desktops, or your distro’s KDE MTP stack).
+ desktops, or your distro's KDE MTP stack).
2. With DBI MTP active, open **Files** / **Dolphin** / **Thunar** and select
the Switch / DBI device → **`1: SD Card`**.
3. Extract the release zip at SD root (merge), or copy into `switch/gen1recomp/`
and the save-dir inboxes as above.
4. Use **only one** MTP accessor at a time. If `mtp-tools` / `mtpfs` reports
- “device is busy”, close the file manager’s MTP mount (or the CLI mount)
+ "device is busy", close the file manager's MTP mount (or the CLI mount)
and retry with a single client.
5. Eject/unmount cleanly; exit MTP on the Switch; title-override launch.
If MTP is unavailable or flaky on Linux, use **direct SD** (Hekate UMS or a
-card reader) or **FTP** instead — same destinations in the table above.
+card reader) or **FTP** instead. Same destinations in the table above.
#### Windows
@@ -93,7 +92,7 @@ card reader) or **FTP** instead — same destinations in the table above.
5. Safely disconnect; exit MTP on the Switch; title-override launch.
If MTP is unavailable or flaky on Windows, use **direct SD** (Hekate UMS or a
-card reader) or **FTP** instead — same destinations in the table above.
+card reader) or **FTP** instead. Same destinations in the table above.
### 2. Direct SD (Hekate UMS or card reader)
@@ -109,9 +108,9 @@ Do not yank the card or unplug UMS mid-write.
### 3. FTP (any SD-exposing Switch FTP)
-Any homebrew FTP server that can write the microSD is fine — for example
-**DBI’s own FTP**, **sys-ftpd-light**, or **Sphaira** (names are illustrations
-only; pick what your CFW setup already uses).
+Any homebrew FTP server that can write the microSD is fine. For example
+**DBI's own FTP**, **sys-ftpd-light**, or **Sphaira**. Names are illustrations
+only; pick what your CFW setup already uses.
1. Start the FTP server on the Switch; note IP/port/credentials from that app.
2. From the host, connect with any FTP client and upload to the same
@@ -154,7 +153,7 @@ Copy the file back from the SD and compare hashes. Round-trip must match.
| Symptom | What to try |
| ------- | ----------- |
| Device busy / no MTP volume | One client only; different cable/port; Windows MTP USB Device driver; alternate method (SD or FTP) |
-| Zip/ROM/`.sav` “could not be opened” | Delete `._*` sidecars (including `._*.sav`); confirm real zip starts with `PK` |
+| Zip/ROM/`.sav` "could not be opened" | Delete `._*` sidecars (including `._*.sav`); confirm real zip starts with `PK` |
| Half-copied NRO / crash on boot | Re-copy; verify SHA-256; exit transfer mode before launch |
| App opens in Applet Mode | Use title override (hold **R**), not Album |
@@ -164,5 +163,3 @@ Copy the file back from the SD and compare hashes. Round-trip must match.
- Players: [switch-install.md](switch-install.md)
- Builders: [switch-build.md](switch-build.md)
-- Status / hardware matrix: [switch-development.md](switch-development.md)
-- Evidence log: [switch-hardware-evidence.md](switch-hardware-evidence.md)
diff --git a/docs/updater.md b/docs/updater.md
index b0de76c8..c27615a3 100644
--- a/docs/updater.md
+++ b/docs/updater.md
@@ -124,3 +124,8 @@ bundled game, in that case.
`love.filesystem.isFused()` is false, and a working tree's `engine` is the
`"0.0.0-dev"` placeholder that always reports up to date, so a source
checkout is always "the game" itself; updating it means pulling the repo.
+- **Nintendo Switch does not use this LÖVE self-updater.** On NX,
+ `Platform.networkValidated()` is `false`, so `Boot.run` / `Check` never
+ download `.love` payloads. In-console OTA uses the **native OTA launcher**
+ (DEVKITPRO), documented in [switch-install.md](switch-install.md). Wire
+ format: `src/update/SwitchOta.lua`. Manual zip install remains the fallback.
diff --git a/libs/flexlove/FlexLove.lua b/libs/flexlove/FlexLove.lua
deleted file mode 100644
index 2f72f720..00000000
--- a/libs/flexlove/FlexLove.lua
+++ /dev/null
@@ -1,1953 +0,0 @@
-local packageName = ... or "FlexLove"
-local modulePath = packageName:match("(.-)[^%.]+$") -- Get the module path prefix (e.g., "libs." or "")
--- If modulePath is empty (e.g., require("FlexLove")), use the package name
-if modulePath == "" then
- modulePath = packageName .. "."
-end
-
-local function req(name)
- return require(modulePath .. "modules." .. name)
-end
-
----@type ErrorHandler
-local ErrorHandler = req("ErrorHandler")
-local ModuleLoader = req("ModuleLoader")
-ModuleLoader.init({ ErrorHandler = ErrorHandler })
-
-local function safeReq(name, isOptional)
- local module = ModuleLoader.safeRequire(modulePath .. "modules." .. name, isOptional)
- if isOptional and module and module._isStub then
- return nil
- end
- return module
-end
-
--- Required core modules
-local utils = req("utils")
-local Calc = req("Calc")
-local Units = req("Units")
-local Context = req("Context")
----@type StateManager
-local StateManager = req("StateManager")
-local RoundedRect = req("RoundedRect")
-local Grid = req("Grid")
-local InputEvent = req("InputEvent")
-local TextEditor = req("TextEditor")
----@type LayoutEngine
-local LayoutEngine = req("LayoutEngine")
-local Renderer = req("Renderer")
----@type EventHandler
-local EventHandler = req("EventHandler")
-local ScrollManager = req("ScrollManager")
----@type ZIndex
-local ZIndex = req("ZIndex")
----@type Element
-local Element = req("Element")
----@type Color
-local Color = req("Color")
-
--- Lua 5.2+ compatibility for unpack (bare global `unpack` is nil under
--- Lua 5.4, which the stock test runner uses). mirrors the shim in
--- modules/Blur.lua:2 and modules/Element.lua:162.
-local unpack = table.unpack or unpack
-
----@type Select
-local Select = req("Select")
-
--- Behavior: mouse/touch event handling, pressed-state, hit-testing (task 02).
--- Auto-attaches to interactive elements via shouldAttach(props).
-local Clickable = req("behaviors.Clickable")
-
--- Behavior: Renderer ownership + theme-state rendering (task 07). Owns the
--- single Renderer:draw call and creates the per-element Renderer. Attaches to
--- every renderable element (see behaviors/Themed.lua for the always-attach
--- rationale). Must precede Clickable in the registry so its core Renderer:draw
--- runs before Clickable's pressed-state overlay (onDraw layering).
-local Themed = req("behaviors.Themed")
-
--- Behavior: image loading + image rendering config (task 07). Enriches the
--- shared element._renderer with image config, runs the deferred image-load
--- pipeline, and persists _loadedImage across immediate-mode frames. Attaches to
--- elements with imagePath/image.
-local Imageable = req("behaviors.Imageable")
-
--- Behavior: animation update, interpolation, chaining, transition wiring
--- (task 06). Auto-attaches to elements that pre-declare `transitions`, and
--- late-attaches on demand via Animated.ensureAttached when an animation is
--- created post-construction (animateTo/fadeIn/direct assignment/transition fire).
-local Animated = req("behaviors.Animated")
-
--- Behavior: Select state-machine lifecycle (task 05). Owns select subsystem
--- init, managed-frame layout sync each frame, and select save/restore. Auto-
--- attaches to elements with selectParent or selectOption props.
-local Selectable = req("behaviors.Selectable")
-
--- Behavior: TextEditor subsystem ownership — text editing, cursor management,
--- text selection, text-related input handling, and text-editor save/restore
--- (task 04). Auto-attaches to editable elements and text-bearing elements via
--- shouldAttach(props); onAttach allocates the TextEditor (editable only).
--- Element retains 1-line forwarders routed through this module for the 27
--- text-editor delegate methods, eliminating the `if self._textEditor` guards.
-local TextEditable = req("behaviors.TextEditable")
-
--- Behavior: ScrollManager lifecycle (task 03, landed via the task 08 capstone).
--- Owns ScrollManager creation + immediate-mode scrollbar interaction-state
--- restore (formerly Element:_initScrollManager). Auto-attaches to elements that
--- declare overflow / overflowX / overflowY. Placed late in the registry: its
--- onAttach creates the ScrollManager, which no other behavior's onAttach
--- depends on. The ScrollManager update / scrollbar draw / state save-restore
--- stay inline in Element:update / Element:draw / Element:saveState as
--- unconditional 1-line delegates (task 09 folds them into hooks).
-local Scrollable = req("behaviors.Scrollable")
-
--- Behavior: generic public-property persistence across the immediate-mode
--- recreation cycle (task 12). Owns the `_props` snapshot (event-driven mutations
--- to `text` / `display` / `opacity` / ... that must survive per-frame Element
--- recreation). Auto-attaches to every element; placed LAST in the registry so
--- its restoreState overrides subsystem-hydrated state, preserving the legacy
--- restore ordering (behaviors first, `_props` tail). With this behavior in
--- place, Element:saveState / Element:restoreState collapse to a pure
--- behavior-dispatch loop and Element owns zero property-extraction logic.
-local Persistable = req("behaviors.Persistable")
-
--- Optional modules (can be excluded in minimal builds)
-local Blur = safeReq("Blur", true)
----@type Performance
-local Performance = safeReq("Performance", true)
----@type KeyboardNavigation
-local KeyboardNavigation = safeReq("KeyboardNavigation", true)
----@type FocusIndicator
-local FocusIndicator = safeReq("FocusIndicator", true)
-local ImageRenderer = safeReq("ImageRenderer", true)
-local ImageScaler = safeReq("ImageScaler", true)
-local NinePatch = safeReq("NinePatch", true)
-local ImageCache = safeReq("ImageCache", true)
-local GestureRecognizer = safeReq("GestureRecognizer", true)
----@type PropertySchema
-local PropertySchema = req("PropertySchema")
----@type Animation
-local Animation = safeReq("Animation", true)
----@type Theme
-local Theme = safeReq("Theme", true)
-
--- Handle Animation.Transform safely
-local Transform = Animation and Animation.Transform or nil
-
-local enums = utils.enums
-
-local flexlove = Context
-flexlove._VERSION = "0.15.0"
-flexlove._DESCRIPTION = "UI Library for LÖVE Framework based on flexbox"
-flexlove._URL = "https://github.com/mikefreno/FlexLove"
-flexlove._LICENSE = [[
- MIT License
-
- Copyright (c) 2025 Mike Freno
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
-]]
-
--- GC (Garbage Collection) configuration
----@type GCConfig
-flexlove._gcConfig = {
- strategy = "auto", -- "auto", "periodic", "manual", "disabled"
- memoryThreshold = 100, -- MB before forcing GC
- interval = 60, -- Frames between GC steps (for periodic mode)
- stepSize = 200, -- Work units per GC step (higher = more aggressive)
-}
----@type GCState
-flexlove._gcState = {
- framesSinceLastGC = 0,
- lastMemory = 0,
- gcCount = 0,
-}
-
--- Deferred callback queue for operations that cannot run while Canvas is active
----@type function[]
-flexlove._deferredCallbacks = {}
-
--- Track accumulated delta time for immediate mode updates
-flexlove._accumulatedDt = 0
-
--- Touch ownership tracking: maps touch ID (string) to the element that owns it
----@type table
-flexlove._touchOwners = {}
-
--- Touch-drag scroll tracking: survives immediate-mode element recreation.
--- Maps touch ID -> { id, lastX, lastY }. Scroll position is persisted into
--- StateManager on every move (same contract as wheelmoved).
----@type table
-flexlove._touchScroll = {}
-
----@type table
-flexlove._mouseButtonStates = {}
-
--- Shared GestureRecognizer instance for touch routing (initialized in init())
----@type GestureRecognizer|nil
-flexlove._gestureRecognizer = nil
-
---- Check if FlexLove initialization is complete and ready to create elements
---- Use this before creating elements to avoid automatic queueing
----@return boolean ready True if FlexLove is initialized and ready to use
-function flexlove.isReady()
- return flexlove._initState == "ready"
-end
-
---- Set up FlexLove for your application's specific needs - configure responsive scaling, theming, rendering mode, and debugging tools
---- Use this to establish a consistent UI foundation that adapts to different screen sizes and provides performance insights
---- After initialization, any queued element creation calls will be automatically processed
----@param config FlexLoveConfig?
-function flexlove.init(config)
- flexlove._initState = "initializing"
- config = config or {}
-
- flexlove._ErrorHandler = ErrorHandler.init({
- includeStackTrace = config.includeStackTrace,
- logLevel = config.reportingLogLevel,
- logTarget = config.errorLogTarget,
- logFile = config.errorLogFile,
- maxLogSize = config.errorLogMaxSize,
- maxLogFiles = config.maxErrorLogFiles,
- enableRotation = config.errorLogRotateEnabled,
- })
-
- -- Initialize Performance if available
- if Performance then
- flexlove._Performance = Performance.init({
- enabled = config.performanceMonitoring or true,
- hudEnabled = false, -- Start with HUD disabled
- hudToggleKey = config.performanceHudKey or "f3",
- hudPosition = config.performanceHudPosition or { x = 10, y = 10 },
- warningThresholdMs = config.performanceWarningThreshold or 13.0,
- criticalThresholdMs = config.performanceCriticalThreshold or 16.67,
- logToConsole = config.performanceLogToConsole or false,
- logWarnings = config.performanceWarnings or false,
- warningsEnabled = config.performanceWarnings or false,
- memoryProfiling = config.memoryProfiling or config.immediateMode and true or false,
- }, { ErrorHandler = flexlove._ErrorHandler })
-
- if config.immediateMode then
- flexlove._Performance:registerTableForMonitoring(
- "StateManager.stateStore",
- StateManager._getInternalState().stateStore
- )
- flexlove._Performance:registerTableForMonitoring(
- "StateManager.stateMetadata",
- StateManager._getInternalState().stateMetadata
- )
- end
- else
- flexlove._Performance = Performance
- end
-
- -- Initialize optional modules if available
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.ImageRenderer") then
- ImageRenderer.init({ ErrorHandler = flexlove._ErrorHandler, utils = utils })
- end
-
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.ImageScaler") then
- ImageScaler.init({ ErrorHandler = flexlove._ErrorHandler })
- end
-
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.NinePatch") then
- NinePatch.init({ ErrorHandler = flexlove._ErrorHandler })
- end
-
- -- Initialize Blur module with immediate mode optimization config
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.Blur") then
- local blurOptimizations = config.immediateModeBlurOptimizations
- if blurOptimizations == nil then
- blurOptimizations = true -- Default to enabled
- end
- Blur.init({
- ErrorHandler = flexlove._ErrorHandler,
- immediateModeOptimizations = blurOptimizations and config.immediateMode or false,
- })
- end
-
- -- Initialize required modules
- StateManager.init({ ErrorHandler = flexlove._ErrorHandler })
- Calc.init({ ErrorHandler = flexlove._ErrorHandler })
- Units.init({ Context = Context, ErrorHandler = flexlove._ErrorHandler, Calc = Calc })
- Color.init({ ErrorHandler = flexlove._ErrorHandler })
- utils.init({ ErrorHandler = flexlove._ErrorHandler })
-
- -- Initialize optional ImageCache module
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.ImageCache") then
- ImageCache.init({ ErrorHandler = flexlove._ErrorHandler })
- end
-
- -- Initialize optional Animation module
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.Animation") then
- Animation.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color })
- end
-
- -- Initialize optional Theme module
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.Theme") then
- Theme.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color, utils = utils })
- end
-
- LayoutEngine.init({ ErrorHandler = flexlove._ErrorHandler, Performance = flexlove._Performance, utils = utils })
- EventHandler.init({
- ErrorHandler = flexlove._ErrorHandler,
- Performance = flexlove._Performance,
- InputEvent = InputEvent,
- utils = utils,
- Context = Context,
- })
-
- -- Initialize shared GestureRecognizer for touch routing
- if GestureRecognizer then
- flexlove._gestureRecognizer = GestureRecognizer.new({}, { InputEvent = InputEvent, utils = utils })
- end
-
- -- Initialize KeyboardNavigation and FocusIndicator if enabled
- local keyboardConfig = config.keyboardNavigation
- if
- KeyboardNavigation
- and (keyboardConfig == true or (type(keyboardConfig) == "table" and keyboardConfig.enabled ~= false))
- then
- KeyboardNavigation.init({
- Context = Context,
- Element = Element,
- ErrorHandler = flexlove._ErrorHandler,
- utils = utils,
- InputEvent = InputEvent,
- })
-
- if FocusIndicator then
- FocusIndicator.init({ Context = Context, Color = Color })
- KeyboardNavigation.FocusIndicator = FocusIndicator
- -- Also set FocusIndicator reference in EventHandler for clearing on mouse click
- EventHandler._FocusIndicator = FocusIndicator
- -- Note: FocusIndicator is only updated from keyboard navigation (_focusElement)
- -- Mouse clicks and activation clear the indicator
- end
-
- -- Apply configuration if provided
- flexlove._applyKeyboardNavConfig(keyboardConfig)
- end
-
- flexlove._defaultDependencies = {
- Context = Context,
- Theme = Theme,
- Color = Color,
- Calc = Calc,
- Units = Units,
- Blur = Blur,
- ImageRenderer = ImageRenderer,
- ImageScaler = ImageScaler,
- NinePatch = NinePatch,
- RoundedRect = RoundedRect,
- ImageCache = ImageCache,
- utils = utils,
- Grid = Grid,
- InputEvent = InputEvent,
- GestureRecognizer = GestureRecognizer,
- StateManager = StateManager,
- TextEditor = TextEditor,
- LayoutEngine = LayoutEngine,
- Renderer = Renderer,
- EventHandler = EventHandler,
- ScrollManager = ScrollManager,
- ErrorHandler = flexlove._ErrorHandler,
- Performance = flexlove._Performance,
- Transform = Transform,
- Animation = Animation,
- ZIndex = ZIndex,
- Select = Select,
- PropertySchema = PropertySchema,
- -- Behavior registry (behavior-mode-unification task 09). Two ordering
- -- invariants:
- -- * Update: Animated (geometry) → Scrollable (scroll interaction) →
- -- Clickable (hit-testing) — animated geometry must be current for
- -- hit-testing, and scrollbar press state must be set before Clickable's
- -- EventHandler processes mouse events.
- -- * Draw: Themed (core Renderer:draw) runs before Clickable (pressed-state
- -- overlay); Scrollable (drawLayer="overlay") is dispatched AFTER children
- -- for scrollbar-on-top. Imageable/Animated/Selectable/TextEditable onDraw
- -- are no-ops, so their position is unconstrained for layering.
- -- 7 entries. (task 09 reordered Animated+Scrollable ahead of Clickable.)
- clickableBehaviors = { Themed, Clickable, Imageable },
- -- Persistable is the registry tail (task 12): its restoreState applies the
- -- `_props` override AFTER every subsystem behavior has hydrated, preserving
- -- the legacy restore ordering (behaviors first, `_props` last).
- behaviors = { Themed, Animated, Scrollable, Clickable, Imageable, Selectable, TextEditable, Persistable },
- TextEditable = TextEditable,
- }
-
- -- Initialize Element module with dependencies
- Element.init(flexlove._defaultDependencies)
-
- if config.baseScale then
- flexlove.baseScale = {
- width = config.baseScale.width or 1920,
- height = config.baseScale.height or 1080,
- }
-
- local currentWidth, currentHeight = Units.getViewport()
- flexlove.scaleFactors.x = currentWidth / flexlove.baseScale.width
- flexlove.scaleFactors.y = currentHeight / flexlove.baseScale.height
- end
-
- if config.theme and ModuleLoader.isModuleLoaded(modulePath .. "modules.Theme") then
- local success, err = pcall(function()
- if type(config.theme) == "string" then
- Theme.load(config.theme)
- Theme.setActive(config.theme)
- flexlove.defaultTheme = config.theme
- elseif type(config.theme) == "table" then
- local theme = Theme.new(config.theme)
- Theme.setActive(theme)
- flexlove.defaultTheme = theme.name
- end
- end)
-
- if not success then
- flexlove._ErrorHandler:warn("FlexLove", "THM_005", {
- error = tostring(err),
- })
- end
- end
-
- local immediateMode = config.immediateMode or false
- flexlove.setMode(immediateMode and "immediate" or "retained")
-
- flexlove._autoFrameManagement = config.autoFrameManagement or false
-
- -- Configure GC strategy
- if config.gcStrategy then
- flexlove._gcConfig.strategy = config.gcStrategy
- end
- if config.gcMemoryThreshold then
- flexlove._gcConfig.memoryThreshold = config.gcMemoryThreshold
- end
- if config.gcInterval then
- flexlove._gcConfig.interval = config.gcInterval
- end
- if config.gcStepSize then
- flexlove._gcConfig.stepSize = config.gcStepSize
- end
-
- if config.stateRetentionFrames or config.maxStateEntries then
- StateManager.configure({
- stateRetentionFrames = config.stateRetentionFrames,
- maxStateEntries = config.maxStateEntries,
- })
- end
- flexlove.initialized = true
- flexlove._initState = "ready"
-
- -- Configure debug draw overlay
- flexlove._debugDraw = config.debugDraw or false
- flexlove._debugDrawKey = config.debugDrawKey or nil
-
- -- Process all queued element creations
- local queue = flexlove._initQueue
- flexlove._initQueue = {} -- Clear queue before processing to prevent re-entry issues
-
- for _, item in ipairs(queue) do
- local element = Element.new(item.props)
- if item.callback and type(item.callback) == "function" then
- local success, err = pcall(item.callback, element)
- if not success then
- flexlove._ErrorHandler:warn(
- "FlexLove",
- string.format("Failed to execute queued element callback: %s", tostring(err))
- )
- end
- end
- end
-end
-
---- Enable keyboard navigation after initialization (for deferred or conditional setup)
---- Useful when you need to conditionally enable keyboard navigation based on runtime conditions
---- Automatically initializes KeyboardNavigation and FocusIndicator modules if not already initialized
----@param config KeyboardNavigationConfig? Optional configuration table
---- @usage
---- -- Enable with defaults
---- FlexLove.enableKeyboardNavigation()
----
---- -- Enable with custom configuration
---- FlexLove.enableKeyboardNavigation({
---- directionalNavigation = true,
---- wrapAround = false,
---- focusIndicator = {
---- enabled = true,
---- color = {1, 0.8, 0, 0.8},
---- lineWidth = 3,
---- },
---- })
---- Enable debug mode for keyboard navigation
---- Use this to troubleshoot keyboard navigation issues
----@param enabled boolean
-function flexlove.setKeyboardNavigationDebug(enabled)
- if KeyboardNavigation and KeyboardNavigation.config then
- KeyboardNavigation.config.debugMode = enabled
- print(string.format("[FlexLove] Keyboard navigation debug mode: %s", tostring(enabled)))
- end
-end
-
---- Apply keyboard navigation configuration (internal helper)
----@param config table
-function flexlove._applyKeyboardNavConfig(config)
- if type(config) ~= "table" then
- return
- end
-
- if config.enabled ~= nil then
- KeyboardNavigation.config.enabled = config.enabled
- end
- if config.directionalNavigation ~= nil then
- KeyboardNavigation.config.directionalNavigation = config.directionalNavigation
- end
- if config.wrapAround ~= nil then
- KeyboardNavigation.config.wrapAround = config.wrapAround
- end
- if config.dropFocusOnSelection ~= nil then
- KeyboardNavigation.config.dropFocusOnSelection = config.dropFocusOnSelection
- end
-
- if config.focusIndicator and FocusIndicator then
- local fiConfig = config.focusIndicator
- if fiConfig.enabled ~= nil then
- FocusIndicator.config.enabled = fiConfig.enabled
- end
- if fiConfig.draw ~= nil then
- FocusIndicator.config.draw = fiConfig.draw
- end
- if fiConfig.color then
- FocusIndicator.setColor(
- fiConfig.color[1] or 0.2,
- fiConfig.color[2] or 0.6,
- fiConfig.color[3] or 1.0,
- fiConfig.color[4] or 0.8
- )
- end
- if fiConfig.lineWidth ~= nil then
- FocusIndicator.config.lineWidth = fiConfig.lineWidth
- end
- if fiConfig.pulseEnabled ~= nil then
- FocusIndicator.config.pulseEnabled = fiConfig.pulseEnabled
- end
- end
-end
-
---- Enable keyboard navigation after initialization (for deferred or conditional setup)
---- Useful when you need to conditionally enable keyboard navigation based on runtime conditions
---- Automatically initializes KeyboardNavigation and FocusIndicator modules if not already initialized
----@usage
---- -- Enable with defaults
---- FlexLove.enableKeyboardNavigation()
----
---- -- Enable with custom configuration
---- FlexLove.enableKeyboardNavigation({
---- directionalNavigation = true,
---- wrapAround = false,
---- dropFocusOnSelection = false,
---- focusIndicator = {
---- enabled = true,
---- color = {1, 0.8, 0, 0.8},
---- lineWidth = 3,
---- draw = function(element, bounds, style) end,
---- },
---- })
----@param config KeyboardNavigationConfig
-function flexlove.enableKeyboardNavigation(config)
- if not KeyboardNavigation then
- return
- end
- config = config or {}
-
- -- Check if already initialized
- if KeyboardNavigation.config and KeyboardNavigation._deps then
- -- Already initialized, just apply config if provided
- flexlove._applyKeyboardNavConfig(config)
- return
- end
-
- -- Initialize KeyboardNavigation
- KeyboardNavigation.init({
- Context = Context,
- Element = Element,
- ErrorHandler = flexlove._ErrorHandler,
- utils = utils,
- InputEvent = InputEvent,
- })
-
- -- Initialize FocusIndicator if available
- if FocusIndicator then
- FocusIndicator.init({ Context = Context, Color = Color })
- KeyboardNavigation.FocusIndicator = FocusIndicator
- -- Also set FocusIndicator reference in EventHandler for clearing on mouse click
- EventHandler._FocusIndicator = FocusIndicator
- -- Note: FocusIndicator is only updated from keyboard navigation (_focusElement)
- -- Mouse clicks and activation clear the indicator
- end
-
- flexlove._applyKeyboardNavConfig(config)
-end
-
---- Safely schedule operations that modify LÖVE's rendering state (like window mode changes) to execute after all canvas operations complete
---- Prevents crashes from attempting canvas-incompatible operations during rendering
----@param callback function The callback to execute
-function flexlove.deferCallback(callback)
- if type(callback) ~= "function" then
- flexlove._ErrorHandler:warn("FlexLove", "CORE_001")
- return
- end
- table.insert(flexlove._deferredCallbacks, callback)
-end
-
---- Execute deferred operations at the safest point in the render cycle - after all canvas operations are complete
---- Call this at the end of love.draw() to enable window resizing and other state-modifying operations without crashes
---- @usage
---- function love.draw()
---- love.graphics.setCanvas(myCanvas)
---- FlexLove.draw()
---- love.graphics.setCanvas() -- Release ALL canvases
---- FlexLove.executeDeferredCallbacks() -- Now safe to execute
---- end
-function flexlove.executeDeferredCallbacks()
- if #flexlove._deferredCallbacks == 0 then
- return
- end
-
- -- Copy callbacks and clear queue before execution
- -- This prevents infinite loops if callbacks defer more callbacks
- local callbacks = flexlove._deferredCallbacks
- flexlove._deferredCallbacks = {}
-
- for _, callback in ipairs(callbacks) do
- local success, err = xpcall(callback, debug.traceback)
- if not success then
- flexlove._ErrorHandler:warn("FlexLove", "CORE_002", {
- error = tostring(err),
- })
- end
- end
-end
-
---- Recalculate all UI layouts when the window size changes - ensures your interface adapts seamlessly to new dimensions
---- Hook this to love.resize() to maintain proper scaling and positioning across window size changes
-function flexlove.resize()
- local newWidth, newHeight = love.window.getMode()
-
- if flexlove.baseScale then
- flexlove.scaleFactors.x = newWidth / flexlove.baseScale.width
- flexlove.scaleFactors.y = newHeight / flexlove.baseScale.height
- end
-
- if ModuleLoader.isModuleLoaded(modulePath .. "modules.Blur") then
- Blur.clearCache()
- end
-
- -- Release old canvases explicitly
- if flexlove._gameCanvas then
- flexlove._gameCanvas:release()
- end
- if flexlove._backdropCanvas then
- flexlove._backdropCanvas:release()
- end
-
- flexlove._gameCanvas = nil
- flexlove._backdropCanvas = nil
- flexlove._canvasDimensions = { width = 0, height = 0 }
-
- for _, win in ipairs(flexlove.topElements) do
- win:resize(newWidth, newHeight)
- end
-end
-
---- Switch between immediate mode (React-like, recreates UI each frame) and retained mode (persistent elements) to match your architectural needs
---- Use immediate for simpler state management and declarative UIs, retained for performance-critical applications with complex state
----@param mode "immediate"|"retained"
-function flexlove.setMode(mode)
- if mode == "immediate" then
- flexlove._immediateMode = true
- flexlove._immediateModeState = StateManager
- flexlove._frameStarted = false
- flexlove._autoBeganFrame = false
- -- Notify StateManager of mode change
- StateManager.setImmediateMode(true)
- elseif mode == "retained" then
- flexlove._immediateMode = false
- flexlove._immediateModeState = nil
- flexlove._frameStarted = false
- flexlove._autoBeganFrame = false
- flexlove._currentFrameElements = {}
- flexlove._frameNumber = 0
- -- Notify StateManager of mode change
- StateManager.setImmediateMode(false)
- else
- error("[FlexLove] Invalid mode: " .. tostring(mode) .. ". Expected 'immediate' or 'retained'")
- end
-end
-
---- Check which rendering mode is active to conditionally handle state management logic
---- Useful for libraries and reusable components that need to adapt to different rendering strategies
----@return "immediate"|"retained"
-function flexlove.getMode()
- return flexlove._immediateMode and "immediate" or "retained"
-end
-
---- Manually start a new frame in immediate mode for precise control over the UI lifecycle
---- Only needed when you want explicit frame boundaries; otherwise FlexLove auto-manages frames
-function flexlove.beginFrame()
- if not flexlove._immediateMode then
- return
- end
-
- -- Reset accumulated delta time for new frame
- flexlove._accumulatedDt = 0
-
- -- Start performance frame timing
- if flexlove._Performance then
- flexlove._Performance:startFrame()
- end
-
- -- Cleanup elements from PREVIOUS frame (after they've been drawn)
- -- This breaks circular references and allows GC to collect memory
- if flexlove._currentFrameElements then
- local function cleanupChildren(elem)
- for _, child in ipairs(elem.children) do
- cleanupChildren(child)
- end
- elem:_cleanup()
- end
-
- for _, element in ipairs(flexlove._currentFrameElements) do
- if not element.parent then
- cleanupChildren(element)
- end
- end
- end
-
- flexlove._frameNumber = flexlove._frameNumber + 1
- StateManager.incrementFrame()
- flexlove._currentFrameElements = {}
- flexlove._frameStarted = true
- flexlove.topElements = {}
-
- Context.clearFrameElements()
-end
-
---- Finalize the frame in immediate mode, triggering layout calculations and state persistence
---- Only needed when manually controlling frames with beginFrame(); otherwise handled automatically
-function flexlove.endFrame()
- if not flexlove._immediateMode then
- return
- end
-
- Context.sortElementsByZIndex()
-
- -- Layout all top-level elements now that all children have been added
- for _, element in ipairs(flexlove._currentFrameElements) do
- if not element.parent then
- element:layoutChildren()
- end
- end
-
- flexlove._handleSelectPointerDismissal()
-
- -- Update all top-level elements created this frame
- for _, element in ipairs(flexlove._currentFrameElements) do
- if not element.parent then
- element:update(flexlove._accumulatedDt)
- end
- end
-
- -- Save state for all elements created this frame
- for _, element in ipairs(flexlove._currentFrameElements) do
- if element.id and element.id ~= "" then
- local stateUpdate = element:saveState()
- local stateChanged = StateManager.updateStateIfChanged(element.id, stateUpdate)
- if stateChanged and (element.backdropBlur or element.contentBlur) and Blur then
- Blur.clearElementCache(element.id)
- end
- end
- end
-
- StateManager.cleanup()
- StateManager.forceCleanupIfNeeded()
- -- Flush dirty state from this frame (no-op in retained mode)
- StateManager.flushFrame()
- flexlove._frameStarted = false
-
- -- End performance frame timing
- if flexlove._Performance then
- flexlove._Performance:endFrame()
- flexlove._Performance:resetFrameCounters()
- end
-end
-
----@type love.Canvas?
-flexlove._gameCanvas = nil
----@type love.Canvas?
-flexlove._backdropCanvas = nil
----@type {width: number, height: number}
-flexlove._canvasDimensions = { width = 0, height = 0 }
-
---- Recursively draw debug boundaries for an element and all its children
---- Draws regardless of visibility/opacity to reveal hidden or transparent elements
----@param element Element
-local function drawDebugElement(element)
- local color = element._debugColor
- if color then
- local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
-
- -- Fill with 0.5 opacity
- love.graphics.setColor(color[1], color[2], color[3], 0.5)
- love.graphics.rectangle("fill", element.x, element.y, bw, bh)
-
- -- Border with full opacity, 1px line
- love.graphics.setColor(color[1], color[2], color[3], 1)
- love.graphics.setLineWidth(1)
- love.graphics.rectangle("line", element.x, element.y, bw, bh)
- end
-
- for _, child in ipairs(element.children) do
- drawDebugElement(child)
- end
-end
-
---- Render the debug draw overlay for all elements in the tree
---- Traverses every element regardless of visibility or opacity
-function flexlove._renderDebugOverlay()
- -- Save current graphics state
- local prevR, prevG, prevB, prevA = love.graphics.getColor()
- local prevLineWidth = love.graphics.getLineWidth()
-
- -- Clear any active scissor so debug draws are always visible
- love.graphics.setScissor()
-
- for _, win in ipairs(flexlove.topElements) do
- drawDebugElement(win)
- end
-
- -- Restore graphics state
- love.graphics.setColor(prevR, prevG, prevB, prevA)
- love.graphics.setLineWidth(prevLineWidth)
-end
-
---- Render all UI elements with optional backdrop blur support for glassmorphic effects
---- Place your game scene in gameDrawFunc to enable backdrop blur on UI elements; use postDrawFunc for overlays
----@param gameDrawFunc function|nil pass component draws that should be affected by a backdrop blur
----@param postDrawFunc function|nil pass component draws that should NOT be affected by a backdrop blur
-function flexlove.draw(gameDrawFunc, postDrawFunc)
- if flexlove._immediateMode and flexlove._autoBeganFrame then
- flexlove.endFrame()
- flexlove._autoBeganFrame = false
- end
-
- local outerCanvas = love.graphics.getCanvas()
- local gameCanvas = nil
-
- if type(gameDrawFunc) == "function" then
- local width, height = love.graphics.getDimensions()
-
- if
- not flexlove._gameCanvas
- or flexlove._canvasDimensions.width ~= width
- or flexlove._canvasDimensions.height ~= height
- then
- -- Release old canvases before creating new ones
- if flexlove._gameCanvas then
- flexlove._gameCanvas:release()
- end
- if flexlove._backdropCanvas then
- flexlove._backdropCanvas:release()
- end
-
- flexlove._gameCanvas = love.graphics.newCanvas(width, height)
- flexlove._backdropCanvas = love.graphics.newCanvas(width, height)
- flexlove._canvasDimensions.width = width
- flexlove._canvasDimensions.height = height
- end
-
- gameCanvas = flexlove._gameCanvas
-
- love.graphics.setCanvas(gameCanvas)
- love.graphics.clear()
- gameDrawFunc()
- love.graphics.setCanvas(outerCanvas)
-
- love.graphics.setColor(1, 1, 1, 1)
- love.graphics.draw(gameCanvas, 0, 0)
- end
-
- table.sort(flexlove.topElements, function(a, b)
- return a.z < b.z
- end)
-
- local function hasBackdropBlur(element)
- if element.backdropBlur and element.backdropBlur.radius > 0 then
- return true
- end
- for _, child in ipairs(element.children) do
- if hasBackdropBlur(child) then
- return true
- end
- end
- return false
- end
-
- local needsBackdropCanvas = false
- for _, win in ipairs(flexlove.topElements) do
- if hasBackdropBlur(win) then
- needsBackdropCanvas = true
- break
- end
- end
-
- if needsBackdropCanvas and gameCanvas then
- local backdropCanvas = flexlove._backdropCanvas
- local prevColor = { love.graphics.getColor() }
-
- love.graphics.setCanvas(backdropCanvas)
- love.graphics.clear()
- love.graphics.setColor(1, 1, 1, 1)
- love.graphics.draw(gameCanvas, 0, 0)
-
- love.graphics.setCanvas(outerCanvas)
- love.graphics.setColor(unpack(prevColor))
-
- for _, win in ipairs(flexlove.topElements) do
- -- Check if this element tree has backdrop blur
- local needsBackdrop = hasBackdropBlur(win)
-
- -- Draw element with backdrop blur applied if needed
- if needsBackdrop then
- win:draw(backdropCanvas)
- else
- win:draw(nil)
- end
-
- -- IMPORTANT: Update backdrop canvas for EVERY element (respecting z-index order)
- -- This ensures that lower z-index elements are visible in the backdrop blur
- -- of higher z-index elements
- love.graphics.setCanvas(backdropCanvas)
- love.graphics.setColor(1, 1, 1, 1)
- win:draw(nil)
- love.graphics.setCanvas(outerCanvas)
- end
- else
- for _, win in ipairs(flexlove.topElements) do
- win:draw(nil)
- end
- end
-
- if type(postDrawFunc) == "function" then
- postDrawFunc()
- end
-
- -- Render performance HUD if enabled
- if flexlove._Performance then
- flexlove._Performance:renderHUD()
- end
-
- -- Render focus indicator if keyboard navigation is enabled
- if KeyboardNavigation and KeyboardNavigation.config and KeyboardNavigation.config.enabled and FocusIndicator then
- FocusIndicator:draw()
- end
-
- -- Render debug draw overlay if enabled
- if flexlove._debugDraw then
- flexlove._renderDebugOverlay()
- end
-
- love.graphics.setCanvas(outerCanvas)
-
- -- NOTE: Deferred callbacks are NOT executed here because the calling code
- -- (e.g., main.lua) might still have a canvas active. Callbacks must be
- -- executed by calling FlexLove.executeDeferredCallbacks() at the very end
- -- of love.draw() after ALL canvases have been released.
-end
-
---- Check if element is an ancestor of target
----@param element Element The potential ancestor element
----@param target Element The target element to check
----@return boolean isAncestor True if element is an ancestor of target
-local function isAncestor(element, target)
- local current = target.parent
- while current do
- if current == element then
- return true
- end
- current = current.parent
- end
- return false
-end
-
----@param element Element
----@param results Element[]
-local function collectOpenSelects(element, results)
- if element._selectState and element._selectState.open then
- table.insert(results, element)
- end
-
- for _, child in ipairs(element.children) do
- collectOpenSelects(child, results)
- end
-end
-
-function flexlove._handleSelectPointerDismissal()
- local isLeftDown = love.mouse.isDown(1)
- local wasLeftDown = flexlove._mouseButtonStates[1] or false
-
- if isLeftDown and not wasLeftDown then
- local mx, my = love.mouse.getPosition()
- local target = flexlove.getElementAtPosition(mx, my)
- local openSelects = {}
-
- for _, element in ipairs(flexlove.topElements) do
- collectOpenSelects(element, openSelects)
- end
-
- for _, selectParent in ipairs(openSelects) do
- local containsTarget = target and (target == selectParent or isAncestor(selectParent, target))
- if not containsTarget then
- selectParent:closeSelect()
- end
- end
- end
-
- flexlove._mouseButtonStates[1] = isLeftDown
-end
-
---- Determine which UI element the user is interacting with at a specific screen position
---- Essential for custom input handling, tooltips, or debugging click targets in complex layouts
----@param x number
----@param y number
----@return Element?
-function flexlove.getElementAtPosition(x, y)
- local candidates = {}
- local blockingElements = {}
-
- local function collectHits(element, scrollOffsetX, scrollOffsetY)
- scrollOffsetX = scrollOffsetX or 0
- scrollOffsetY = scrollOffsetY or 0
-
- -- pointHitsElement is the single canonical bounds + display:none guard.
- if Context.pointHitsElement(element, x, y, scrollOffsetX, scrollOffsetY) then
- -- Skip invisible/transparent elements and their entire subtree
- if element.visibility == "hidden" or element.opacity <= 0 then
- return
- end
-
- -- Collect interactive elements (those with onEvent handlers)
- if
- (element.onEvent or element.editable or element._selectState or element.selectOption) and not element.disabled
- then
- table.insert(candidates, element)
- end
-
- -- Collect all visible elements for input blocking
- -- Elements with opacity > 0 block input to elements below them
- if element.opacity > 0 then
- table.insert(blockingElements, element)
- end
-
- -- Check if this element has scrollable overflow
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- local hasScrollableOverflow = (
- overflowX == "scroll"
- or overflowX == "auto"
- or overflowY == "scroll"
- or overflowY == "auto"
- or overflowX == "hidden"
- or overflowY == "hidden"
- )
-
- -- Accumulate scroll offset for children if this element has overflow clipping
- local childScrollOffsetX = scrollOffsetX
- local childScrollOffsetY = scrollOffsetY
- if hasScrollableOverflow then
- childScrollOffsetX = childScrollOffsetX + (element._scrollX or 0)
- childScrollOffsetY = childScrollOffsetY + (element._scrollY or 0)
- end
-
- for _, child in ipairs(element.children) do
- collectHits(child, childScrollOffsetX, childScrollOffsetY)
- end
- end
- end
-
- for _, element in ipairs(flexlove.topElements) do
- collectHits(element)
- end
-
- -- Sort both lists by composite z-index (highest first). Uses the same
- -- rootZ * ROOT_WEIGHT + depth * DEPTH_WEIGHT + ownZ key as
- -- Context.sortElementsByZIndex / findInteractiveAtPosition so the hit-test
- -- topmost matches the visual draw order across overlapping windows.
- -- Skip the precompute + sort when there is 0 or 1 element (the common case
- -- for getElementAtPosition which is called on every love.mousemoved event).
- if #candidates > 1 then
- local candidateZ = {}
- for i = 1, #candidates do
- candidateZ[candidates[i]] = Context.getEffectiveZIndex(candidates[i])
- end
- table.sort(candidates, function(a, b)
- return candidateZ[a] > candidateZ[b]
- end)
- end
-
- if #blockingElements > 1 then
- local blockerZ = {}
- for i = 1, #blockingElements do
- blockerZ[blockingElements[i]] = Context.getEffectiveZIndex(blockingElements[i])
- end
- table.sort(blockingElements, function(a, b)
- return blockerZ[a] > blockerZ[b]
- end)
- end
-
- -- If we have interactive elements, return the topmost one
- -- But only if there's no blocking element with higher z-index (that isn't an ancestor)
- if #candidates > 0 then
- local topCandidate = candidates[1]
-
- -- Check if any blocking element would prevent this interaction
- if #blockingElements > 0 then
- local topBlocker = blockingElements[1]
- -- If the top blocker has higher z-index than the top candidate,
- -- and the blocker is NOT an ancestor of the candidate,
- -- return the blocker (even though it has no onEvent, it blocks input)
- if topBlocker.z > topCandidate.z and not isAncestor(topBlocker, topCandidate) then
- return topBlocker
- end
- end
-
- return topCandidate
- end
-
- -- No interactive elements, but return topmost blocking element if any
- -- This prevents clicks from passing through non-interactive overlays
- return blockingElements[1]
-end
-
---- Update all UI animations, interactions, and state changes each frame
---- Hook this to love.update() to enable hover effects, animations, text cursors, and scrolling
----@param dt number
-function flexlove.update(dt)
- -- Update Performance module with actual delta time for accurate FPS
- if flexlove._Performance then
- flexlove._Performance:updateDeltaTime(dt)
- end
-
- -- Update keyboard navigation (animations, etc.)
- if KeyboardNavigation then
- KeyboardNavigation:update(dt)
- end
-
- -- Garbage collection management
- flexlove._manageGC()
-
- -- Invalidate the per-frame findInteractiveAtPosition cache so Clickable's
- -- per-element occlusion lookup (one per interactive element per frame) is
- -- recomputed fresh for this frame's tree. Within the frame every
- -- Clickable.onUpdate then shares one cached result instead of re-walking
- -- + re-sorting the tree per element (unified-event-routing task 05 fix).
- flexlove.clearInteractiveCache()
-
- -- Select-pointer dismissal: if the left mouse button was just pressed,
- -- check whether any open Select dropdowns should be closed (click-outside).
- -- This calls getElementAtPosition ONLY on the click frame, not every frame.
- flexlove._handleSelectPointerDismissal()
-
- -- In immediate mode, accumulate dt and skip updating here - elements will be updated in endFrame after layout
- if flexlove._immediateMode then
- flexlove._accumulatedDt = flexlove._accumulatedDt + dt
- else
- for _, win in ipairs(flexlove.topElements) do
- win:update(dt)
- end
- end
-
- -- Note: State saving happens in endFrame() after element:update() is called
- -- This ensures all state changes (including cursor blink) are captured once per frame
-end
-
---- Internal GC management function (called from update)
-function flexlove._manageGC()
- local strategy = flexlove._gcConfig.strategy
-
- if strategy == "disabled" then
- return
- end
-
- local currentMemory = collectgarbage("count") / 1024 -- Convert to MB
- flexlove._gcState.lastMemory = currentMemory
- flexlove._gcState.framesSinceLastGC = flexlove._gcState.framesSinceLastGC + 1
-
- -- Check memory threshold (applies to all strategies except disabled)
- if currentMemory > flexlove._gcConfig.memoryThreshold then
- -- Force full GC when exceeding threshold
- collectgarbage("collect")
- flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
- flexlove._gcState.framesSinceLastGC = 0
- return
- end
-
- -- Strategy-specific GC
- if strategy == "periodic" then
- -- Run incremental GC step every N frames
- if flexlove._gcState.framesSinceLastGC >= flexlove._gcConfig.interval then
- collectgarbage("step", flexlove._gcConfig.stepSize)
- flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
- flexlove._gcState.framesSinceLastGC = 0
- end
- elseif strategy == "auto" then
- -- Let Lua's automatic GC handle it, but help with incremental steps
- -- Run a small step every frame to keep memory under control
- if flexlove._gcState.framesSinceLastGC >= 5 then
- collectgarbage("step", 50) -- Small steps to avoid frame drops
- flexlove._gcState.framesSinceLastGC = 0
- end
- end
- -- "manual" strategy: no automatic GC, user must call flexlove.collectGarbage()
-end
-
---- Manually trigger garbage collection to prevent frame drops during critical gameplay moments
---- Use this to control when memory cleanup happens rather than letting it occur unpredictably
----@param mode? string "collect" for full GC, "step" for incremental (default: "collect")
----@param stepSize? number Work units for step mode (default: 200)
-function flexlove.collectGarbage(mode, stepSize)
- mode = mode or "collect"
- stepSize = stepSize or 200
-
- if mode == "collect" then
- collectgarbage("collect")
- flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
- flexlove._gcState.framesSinceLastGC = 0
- elseif mode == "step" then
- collectgarbage("step", stepSize)
- elseif mode == "count" then
- return collectgarbage("count") / 1024 -- Return memory in MB
- end
-end
-
---- Choose how FlexLove manages memory cleanup to balance performance and memory usage for your app's needs
---- Use "manual" for tight control in performance-critical sections, "auto" for hands-off operation
----@param strategy string "auto", "periodic", "manual", or "disabled"
-function flexlove.setGCStrategy(strategy)
- if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then
- flexlove._gcConfig.strategy = strategy
- else
- flexlove._ErrorHandler:warn("FlexLove", "CORE_003", {
- strategy = tostring(strategy),
- })
- end
-end
-
---- Monitor memory management behavior to diagnose performance issues and tune GC settings
---- Use this to identify memory leaks or optimize garbage collection timing
----@return GCStats stats GC statistics
-function flexlove.getGCStats()
- return {
- gcCount = flexlove._gcState.gcCount,
- framesSinceLastGC = flexlove._gcState.framesSinceLastGC,
- currentMemoryMB = flexlove._gcState.lastMemory,
- strategy = flexlove._gcConfig.strategy,
- threshold = flexlove._gcConfig.memoryThreshold,
- }
-end
-
---- Forward text input to focused editable elements like text fields and text areas
---- Hook this to love.textinput() to enable text entry in your UI
----@param text string
-function flexlove.textinput(text)
- local focusedElement = Context.getFocused()
- if focusedElement and not focusedElement.disabled then
- focusedElement:textinput(text)
- end
-end
-
---- Handle keyboard input for text editing, navigation, and performance overlay toggling
---- Hook this to love.keypressed() to enable text selection, cursor movement, and the performance HUD
----@param key string
----@param scancode string
----@param isrepeat boolean
-function flexlove.keypressed(key, scancode, isrepeat)
- if flexlove._Performance then
- flexlove._Performance:keypressed(key)
- end
- if flexlove._debugDrawKey and key == flexlove._debugDrawKey then
- flexlove._debugDraw = not flexlove._debugDraw
- end
-
- -- Handle keyboard navigation (if module is available and enabled)
- if KeyboardNavigation and KeyboardNavigation.config and KeyboardNavigation.config.enabled then
- -- Debug logging for keyboard navigation entry point
- if KeyboardNavigation.config.debugMode then
- print(string.format("[FlexLove.keypressed] Keyboard nav enabled, handling key: %s", key))
- end
-
- -- Check if we're in text input mode (editable element focused)
- local focusedElement = Context.getFocused()
- local isTextInputMode = focusedElement and (focusedElement.editable or focusedElement._textEditor)
-
- -- Only handle navigation if not in text input mode, or if in text input mode without modifiers
- local shouldHandleNav = not isTextInputMode
- or (
- isTextInputMode
- and not (
- love.keyboard.isDown("lctrl")
- or love.keyboard.isDown("rctrl")
- or love.keyboard.isDown("lalt")
- or love.keyboard.isDown("ralt")
- )
- )
-
- if shouldHandleNav then
- local handled = KeyboardNavigation:handleKeyPress(key, scancode, isrepeat)
- if KeyboardNavigation.config.debugMode and not handled then
- print(string.format("[FlexLove.keypressed] Key %s was NOT handled by keyboard navigation", key))
- end
- if handled then
- return -- Navigation handled the key, don't forward to element
- end
- end
- end
-
- -- Forward to focused element for text input
- local focusedElement = Context.getFocused()
- if focusedElement and not focusedElement.disabled then
- focusedElement:keypressed(key, scancode, isrepeat)
- end
-end
-
---- Enable mouse wheel scrolling in scrollable containers and lists
---- Hook this to love.wheelmoved() to allow users to scroll through content naturally
----@param dx number
----@param dy number
-function flexlove.wheelmoved(dx, dy)
- local mx, my = love.mouse.getPosition()
- local element = Context.findScrollableAtPosition(mx, my)
-
- if element then
- element:_handleWheelScroll(dx, dy)
-
- -- In immediate mode, persist scroll manager state for next frame
- if flexlove._immediateMode and element._stateId and element._scrollManager then
- local scrollManagerState = element._scrollManager:getState()
- StateManager.updateState(element._stateId, {
- scrollManager = scrollManagerState,
- })
- end
- end
-end
-
---- Find the touch-interactive element at a given position using z-index ordering
---- Similar to getElementAtPosition but checks for touch-enabled elements
----@param x number Touch X position
----@param y number Touch Y position
----@return Element|nil element The topmost touch-enabled element at position
-function flexlove._getTouchElementAtPosition(x, y)
- local candidates = {}
-
- local function collectTouchHits(element, scrollOffsetX, scrollOffsetY)
- scrollOffsetX = scrollOffsetX or 0
- scrollOffsetY = scrollOffsetY or 0
-
- -- pointHitsElement is the single canonical bounds + display:none guard.
- if Context.pointHitsElement(element, x, y, scrollOffsetX, scrollOffsetY) then
- -- Check if element is touch-enabled and interactive
- if
- element.touchEnabled
- and not element.disabled
- and (element.onEvent or element.onTouchEvent or element.onGesture)
- then
- table.insert(candidates, element)
- end
-
- -- Check if this element has scrollable overflow (for touch scrolling)
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- local hasScrollableOverflow = (
- overflowX == "scroll"
- or overflowX == "auto"
- or overflowY == "scroll"
- or overflowY == "auto"
- or overflowX == "hidden"
- or overflowY == "hidden"
- )
-
- -- Accumulate scroll offset for children
- local childScrollOffsetX = scrollOffsetX
- local childScrollOffsetY = scrollOffsetY
- if hasScrollableOverflow then
- childScrollOffsetX = childScrollOffsetX + (element._scrollX or 0)
- childScrollOffsetY = childScrollOffsetY + (element._scrollY or 0)
- end
-
- for _, child in ipairs(element.children) do
- collectTouchHits(child, childScrollOffsetX, childScrollOffsetY)
- end
- end
- end
-
- for _, element in ipairs(flexlove.topElements) do
- collectTouchHits(element)
- end
-
- -- Sort by z-index (highest first) — topmost element wins
- table.sort(candidates, function(a, b)
- return a.z > b.z
- end)
-
- return candidates[1]
-end
-
-local function elementIsTouchScrollable(element)
- if not element or not element._scrollManager then
- return false
- end
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- return overflowX == "scroll"
- or overflowX == "auto"
- or overflowY == "scroll"
- or overflowY == "auto"
-end
-
--- Walk parents from a hit target so a finger on a button/row still scrolls
--- the containing list. Falls back to the wheel path's scrollable lookup when
--- the press lands on empty space inside a scroller.
-local function findTouchScrollTarget(x, y, startElement)
- local el = startElement
- while el do
- if elementIsTouchScrollable(el) then
- return el
- end
- el = el.parent
- end
- return Context.findScrollableAtPosition(x, y)
-end
-
-local function findElementByStateId(stateId)
- if not stateId or stateId == "" then
- return nil
- end
- local function walk(element)
- if element.id == stateId or element._stateId == stateId then
- return element
- end
- for _, child in ipairs(element.children or {}) do
- local found = walk(child)
- if found then
- return found
- end
- end
- return nil
- end
- for _, element in ipairs(flexlove.topElements or {}) do
- local found = walk(element)
- if found then
- return found
- end
- end
- for _, element in ipairs(flexlove._currentFrameElements or {}) do
- local found = walk(element)
- if found then
- return found
- end
- end
- return nil
-end
-
-local function persistTouchScroll(element)
- if flexlove._immediateMode and element and element._stateId and element._scrollManager then
- StateManager.updateState(element._stateId, {
- scrollManager = element._scrollManager:getState(),
- })
- end
-end
-
-local function resolveTouchScrollElement(track)
- if not track then
- return nil
- end
- local element = findElementByStateId(track.id)
- if elementIsTouchScrollable(element) then
- return element
- end
- return nil
-end
-
---- Handle touch press events from LÖVE's touch input system
---- Routes touch to the topmost element at the touch position and assigns touch ownership
---- Hook this to love.touchpressed() to enable touch interaction
----@param id lightuserdata Touch identifier from LÖVE
----@param x number Touch X position in screen coordinates
----@param y number Touch Y position in screen coordinates
----@param dx number X distance moved (usually 0 on press)
----@param dy number Y distance moved (usually 0 on press)
----@param pressure number Touch pressure (0-1, if supported by device)
-function flexlove.touchpressed(id, x, y, dx, dy, pressure)
- local touchId = tostring(id)
- pressure = pressure or 1.0
-
- -- Apply base scaling if configured
- local touchX, touchY = x, y
- if flexlove.baseScale then
- touchX = x / flexlove.scaleFactors.x
- touchY = y / flexlove.scaleFactors.y
- end
-
- -- Find the topmost touch-enabled element at this position
- local element = flexlove._getTouchElementAtPosition(touchX, touchY)
-
- if element then
- -- Assign touch ownership: this element receives all subsequent events for this touch
- flexlove._touchOwners[touchId] = element
-
- -- Create and route touch event
- local touchEvent = InputEvent.fromTouch(id, touchX, touchY, "began", pressure)
- element:handleTouchEvent(touchEvent)
-
- -- Feed to shared gesture recognizer
- if flexlove._gestureRecognizer then
- local gestures = flexlove._gestureRecognizer:processTouchEvent(touchEvent)
- if gestures then
- for _, gesture in ipairs(gestures) do
- element:handleGesture(gesture)
- end
- end
- end
- end
-
- -- Scroll target is the nearest scrollable ancestor (or the scroller under
- -- empty space). Tracked by stable id so immediate-mode recreation can resume.
- local scrollEl = findTouchScrollTarget(touchX, touchY, element)
- if scrollEl and scrollEl._scrollManager then
- local scrollId = scrollEl._stateId or scrollEl.id
- if scrollId and scrollId ~= "" then
- flexlove._touchScroll[touchId] = {
- id = scrollId,
- lastX = touchX,
- lastY = touchY,
- }
- end
- scrollEl._scrollManager:handleTouchPress(touchX, touchY)
- persistTouchScroll(scrollEl)
- end
-end
-
---- Handle touch move events from LÖVE's touch input system
---- Routes touch to the element that owns this touch ID (from the original press), regardless of current position
---- Hook this to love.touchmoved() to enable touch drag and gesture tracking
----@param id lightuserdata Touch identifier from LÖVE
----@param x number Touch X position in screen coordinates
----@param y number Touch Y position in screen coordinates
----@param dx number X distance moved since last event
----@param dy number Y distance moved since last event
----@param pressure number Touch pressure (0-1, if supported by device)
-function flexlove.touchmoved(id, x, y, dx, dy, pressure)
- local touchId = tostring(id)
- pressure = pressure or 1.0
-
- -- Apply base scaling if configured
- local touchX, touchY = x, y
- if flexlove.baseScale then
- touchX = x / flexlove.scaleFactors.x
- touchY = y / flexlove.scaleFactors.y
- end
-
- -- Route to owning element (touch ownership persists from press to release)
- local element = flexlove._touchOwners[touchId]
- if element then
- -- Create and route touch event
- local touchEvent = InputEvent.fromTouch(id, touchX, touchY, "moved", pressure)
- element:handleTouchEvent(touchEvent)
-
- -- Feed to shared gesture recognizer
- if flexlove._gestureRecognizer then
- local gestures = flexlove._gestureRecognizer:processTouchEvent(touchEvent)
- if gestures then
- for _, gesture in ipairs(gestures) do
- element:handleGesture(gesture)
- end
- end
- end
- end
-
- local track = flexlove._touchScroll[touchId]
- local scrollEl = resolveTouchScrollElement(track)
- if track and scrollEl then
- local sm = scrollEl._scrollManager
- -- Immediate mode recreates managers each frame; re-arm drag from the
- -- last persisted touch point so a move after beginFrame still scrolls.
- if not sm._touchScrolling then
- sm:handleTouchPress(track.lastX, track.lastY)
- end
- sm:handleTouchMove(touchX, touchY)
- persistTouchScroll(scrollEl)
- track.lastX = touchX
- track.lastY = touchY
- end
-end
-
---- Handle touch release events from LÖVE's touch input system
---- Routes touch to the owning element and cleans up touch ownership tracking
---- Hook this to love.touchreleased() to properly end touch interactions
----@param id lightuserdata Touch identifier from LÖVE
----@param x number Touch X position in screen coordinates
----@param y number Touch Y position in screen coordinates
----@param dx number X distance moved since last event
----@param dy number Y distance moved since last event
----@param pressure number Touch pressure (0-1, if supported by device)
-function flexlove.touchreleased(id, x, y, dx, dy, pressure)
- local touchId = tostring(id)
- pressure = pressure or 1.0
-
- -- Apply base scaling if configured
- local touchX, touchY = x, y
- if flexlove.baseScale then
- touchX = x / flexlove.scaleFactors.x
- touchY = y / flexlove.scaleFactors.y
- end
-
- -- Route to owning element
- local element = flexlove._touchOwners[touchId]
- if element then
- -- Create and route touch event
- local touchEvent = InputEvent.fromTouch(id, touchX, touchY, "ended", pressure)
- element:handleTouchEvent(touchEvent)
-
- -- Feed to shared gesture recognizer
- if flexlove._gestureRecognizer then
- local gestures = flexlove._gestureRecognizer:processTouchEvent(touchEvent)
- if gestures then
- for _, gesture in ipairs(gestures) do
- element:handleGesture(gesture)
- end
- end
- end
- end
-
- local track = flexlove._touchScroll[touchId]
- local scrollEl = resolveTouchScrollElement(track)
- if track and scrollEl then
- local sm = scrollEl._scrollManager
- if not sm._touchScrolling then
- sm:handleTouchPress(track.lastX, track.lastY)
- end
- sm:handleTouchMove(touchX, touchY)
- sm:handleTouchRelease()
- persistTouchScroll(scrollEl)
- end
-
- -- Clean up touch ownership (touch is complete)
- flexlove._touchOwners[touchId] = nil
- flexlove._touchScroll[touchId] = nil
-end
-
---- Get the number of currently active touches being tracked
----@return number count Number of active touch points
-function flexlove.getActiveTouchCount()
- local count = 0
- for _ in pairs(flexlove._touchOwners) do
- count = count + 1
- end
- return count
-end
-
---- Get the element that currently owns a specific touch
----@param touchId string|lightuserdata Touch identifier
----@return Element|nil element The element owning this touch, or nil
-function flexlove.getTouchOwner(touchId)
- return flexlove._touchOwners[tostring(touchId)]
-end
-
---- Retrieve an element by its ID from the UI tree
---- Works in both immediate and retained modes; searches all known elements including top-level and nested children
----@param id string The element ID to search for
----@return Element|nil element The found element, or nil if not found
-function flexlove.getById(id)
- if not id or id == "" then
- return nil
- end
-
- local function findElementById(element, targetId)
- if element.id == targetId then
- return element
- end
-
- for _, child in ipairs(element.children) do
- local result = findElementById(child, targetId)
- if result then
- return result
- end
- end
-
- return nil
- end
-
- for _, win in ipairs(flexlove.topElements) do
- local result = findElementById(win, id)
- if result then
- return result
- end
- end
-
- if flexlove._currentFrameElements then
- for _, element in ipairs(flexlove._currentFrameElements) do
- local result = findElementById(element, id)
- if result then
- return result
- end
- end
- end
-
- if Context._zIndexOrderedElements then
- for _, element in ipairs(Context._zIndexOrderedElements) do
- local result = findElementById(element, id)
- if result then
- return result
- end
- end
- end
-
- return nil
-end
-
---- Clean up all UI elements and reset FlexLove to initial state when changing scenes or shutting down
---- Use this to prevent memory leaks when transitioning between game states or menus
-function flexlove.destroy()
- for _, win in ipairs(flexlove.topElements) do
- win:destroy()
- end
- flexlove.topElements = {}
- flexlove.baseScale = nil
- flexlove.scaleFactors = { x = 1.0, y = 1.0 }
- flexlove._cachedViewport = { width = 0, height = 0 }
-
- -- Release canvases explicitly before destroying
- if flexlove._gameCanvas then
- flexlove._gameCanvas:release()
- end
- if flexlove._backdropCanvas then
- flexlove._backdropCanvas:release()
- end
-
- flexlove._gameCanvas = nil
- flexlove._backdropCanvas = nil
- flexlove._canvasDimensions = { width = 0, height = 0 }
- Context.clearFocus()
- StateManager:reset()
-
- -- Clean up touch state
- flexlove._touchOwners = {}
- flexlove._touchScroll = {}
- flexlove._mouseButtonStates = {}
- if flexlove._gestureRecognizer then
- flexlove._gestureRecognizer:reset()
- end
-end
-
---- Create a new UI element with flexbox layout, styling, and interaction capabilities
---- This is your primary API for building interfaces - buttons, panels, text, images, and containers
---- If called before FlexLove.init(), the element creation will be automatically queued and executed after initialization
----@param props ElementProps
----@param callback? function Optional callback function(element) that will be called with the created element (useful when queued)
----@return Element -- Returns element if initialized, nil if queued for later creation
-function flexlove.new(props, callback)
- props = props or {}
-
- if not flexlove.initialized then
- -- Queue element creation for after initialization
- table.insert(flexlove._initQueue, {
- props = props,
- callback = callback,
- })
-
- if flexlove._initState == "uninitialized" then
- if flexlove._ErrorHandler then
- flexlove._ErrorHandler:warn(
- "FlexLove",
- "[FlexLove] Element creation queued - FlexLove.init() has not been called yet. Element will be created automatically after init() is called."
- )
- end
- end
- return nil
- end
-
- -- Use global mode to determine behavior
- if not flexlove._immediateMode then
- return Element.new(props)
- end
-
- -- Immediate mode - proceed with immediate-mode logic
- -- Auto-begin frame if not manually started (convenience feature)
- if not flexlove._frameStarted then
- flexlove.beginFrame()
- flexlove._autoBeganFrame = true
- end
-
- -- Immediate mode: generate ID if not provided
- if not props.id then
- props.id = StateManager.generateID(props, props.parent)
- end
-
- -- Get or create state for this element
- local state = StateManager.getState(props.id, {})
-
- -- Mark state as used this frame
- StateManager.markStateUsed(props.id)
-
- -- Inject scroll state into props BEFORE creating element
- -- This ensures scroll position is set before layoutChildren/detectOverflow is called
- -- ScrollManager state uses _scrollX/_scrollY with underscore prefix
- if state.scrollManager then
- props._scrollX = state.scrollManager._scrollX or 0
- props._scrollY = state.scrollManager._scrollY or 0
- else
- -- Fallback to old state structure for backward compatibility
- props._scrollX = state._scrollX or 0
- props._scrollY = state._scrollY or 0
- end
-
- local element = Element.new(props)
-
- -- Restore all state from StateManager (delegates to sub-modules)
- element:restoreState(state)
-
- -- Bind element to StateManager for interactive states
- element._stateId = props.id
-
- -- Set initial theme state based on StateManager state
- -- This will be updated in Element:update() but we need an initial value
- if element.themeComponent then
- local eventState = state.eventHandler or {}
- if element.disabled or eventState.disabled then
- element._themeState = "disabled"
- elseif element.active or eventState.active then
- element._themeState = "active"
- elseif eventState._pressed and next(eventState._pressed) then
- element._themeState = "pressed"
- elseif eventState._hovered then
- element._themeState = "hover"
- else
- element._themeState = "normal"
- end
- end
-
- table.insert(flexlove._currentFrameElements, element)
-
- return element
-end
-
---- Check how many UI element states are being tracked in immediate mode to detect memory leaks
---- Use this during development to ensure states are properly cleaned up
----@return number
-function flexlove.getStateCount()
- if not flexlove._immediateMode then
- return 0
- end
- return StateManager.getStateCount()
-end
-
---- Remove stored state for a specific element when you know it won't be rendered again
---- Use this to immediately free memory for elements you've removed from your UI
----@param id string
-function flexlove.clearState(id)
- if not flexlove._immediateMode then
- return
- end
- StateManager.clearState(id)
-end
-
---- Wipe all element state when transitioning between completely different UI screens
---- Use this for scene transitions to start with a clean slate and prevent state pollution
-function flexlove.clearAllStates()
- if not flexlove._immediateMode then
- return
- end
- StateManager.clearAllStates()
-end
-
---- Inspect state management metrics to diagnose performance issues and optimize immediate mode usage
---- Use this to understand state lifecycle and identify unexpected state accumulation
----@return { stateCount: number, frameNumber: number, oldestState: number|nil, newestState: number|nil }
-function flexlove.getStateStats()
- if not flexlove._immediateMode then
- return { stateCount = 0, frameNumber = 0 }
- end
- return StateManager.getStats()
-end
-
---- Create a calc() expression for dynamic CSS-like calculations
---- Use this to create responsive layouts that adapt to viewport and parent dimensions
---- @usage
---- local button = FlexLove.new({
---- x = FlexLove.calc("50% - 10vw"),
---- y = FlexLove.calc("50% - 5vh"),
---- width = "20vw",
---- height = "10vh",
---- })
----@param expr string The calc expression (e.g., "50% - 10vw", "100px + 20%")
----@return CalcObject calcObject A calc expression object that will be evaluated during layout
-function flexlove.calc(expr)
- return Calc.new(expr)
-end
-
---- Get the currently focused element
---- Returns the element that is currently receiving keyboard input (e.g., text input, text area)
----@return Element|nil The focused element, or nil if no element has focus
-function flexlove.getFocusedElement()
- return Context.getFocused()
-end
-
---- Set focus to a specific element
---- Automatically blurs the previously focused element if different
---- Use this to programmatically focus text inputs or other interactive elements
----@param element Element|nil The element to focus (nil to clear focus)
-function flexlove.setFocusedElement(element)
- Context.setFocused(element)
-end
-
---- Clear focus from any element
---- Removes keyboard focus from the currently focused element
-function flexlove.clearFocus()
- Context.setFocused(nil)
-end
-
---- Enable or disable the debug draw overlay that renders element boundaries with random colors
---- Each element gets a unique color: full opacity border and 0.5 opacity fill to identify collisions and overlaps
----@param enabled boolean True to enable debug draw overlay, false to disable
-function flexlove.setDebugDraw(enabled)
- flexlove._debugDraw = enabled
-end
-
---- Check if the debug draw overlay is currently active
----@return boolean enabled True if debug draw overlay is enabled
-function flexlove.getDebugDraw()
- return flexlove._debugDraw
-end
-
-flexlove.Animation = Animation
-flexlove.Color = Color
-flexlove.Theme = Theme
-flexlove.enums = enums
-
-return flexlove
diff --git a/libs/flexlove/modules/Animation.lua b/libs/flexlove/modules/Animation.lua
deleted file mode 100644
index 66a3d5d1..00000000
--- a/libs/flexlove/modules/Animation.lua
+++ /dev/null
@@ -1,1579 +0,0 @@
-local Easing = {}
-
----@type EasingFunction
-function Easing.linear(t)
- return t
-end
-
----@type EasingFunction
-function Easing.easeInQuad(t)
- return t * t
-end
-
----@type EasingFunction
-function Easing.easeOutQuad(t)
- return t * (2 - t)
-end
-
----@type EasingFunction
-function Easing.easeInOutQuad(t)
- return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t
-end
-
----@type EasingFunction
-function Easing.easeInCubic(t)
- return t * t * t
-end
-
----@type EasingFunction
-function Easing.easeOutCubic(t)
- local t1 = t - 1
- return t1 * t1 * t1 + 1
-end
-
----@type EasingFunction
-function Easing.easeInOutCubic(t)
- return t < 0.5 and 4 * t * t * t or (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
-end
-
----@type EasingFunction
-function Easing.easeInQuart(t)
- return t * t * t * t
-end
-
----@type EasingFunction
-function Easing.easeOutQuart(t)
- local t1 = t - 1
- return 1 - t1 * t1 * t1 * t1
-end
-
----@type EasingFunction
-function Easing.easeInOutQuart(t)
- if t < 0.5 then
- return 8 * t * t * t * t
- else
- local t1 = t - 1
- return 1 - 8 * t1 * t1 * t1 * t1
- end
-end
-
----@type EasingFunction
-function Easing.easeInQuint(t)
- return t * t * t * t * t
-end
-
----@type EasingFunction
-function Easing.easeOutQuint(t)
- local t1 = t - 1
- return 1 + t1 * t1 * t1 * t1 * t1
-end
-
----@type EasingFunction
-function Easing.easeInOutQuint(t)
- if t < 0.5 then
- return 16 * t * t * t * t * t
- else
- local t1 = t - 1
- return 1 + 16 * t1 * t1 * t1 * t1 * t1
- end
-end
-
----@type EasingFunction
-function Easing.easeInExpo(t)
- return t == 0 and 0 or math.pow(2, 10 * (t - 1))
-end
-
----@type EasingFunction
-function Easing.easeOutExpo(t)
- return t == 1 and 1 or 1 - math.pow(2, -10 * t)
-end
-
----@type EasingFunction
-function Easing.easeInOutExpo(t)
- if t == 0 then
- return 0
- end
- if t == 1 then
- return 1
- end
-
- if t < 0.5 then
- return 0.5 * math.pow(2, 20 * t - 10)
- else
- return 1 - 0.5 * math.pow(2, -20 * t + 10)
- end
-end
-
----@type EasingFunction
-function Easing.easeInSine(t)
- return 1 - math.cos(t * math.pi / 2)
-end
-
----@type EasingFunction
-function Easing.easeOutSine(t)
- return math.sin(t * math.pi / 2)
-end
-
----@type EasingFunction
-function Easing.easeInOutSine(t)
- return -(math.cos(math.pi * t) - 1) / 2
-end
-
----@type EasingFunction
-function Easing.easeInCirc(t)
- return 1 - math.sqrt(1 - t * t)
-end
-
----@type EasingFunction
-function Easing.easeOutCirc(t)
- local t1 = t - 1
- return math.sqrt(1 - t1 * t1)
-end
-
----@type EasingFunction
-function Easing.easeInOutCirc(t)
- if t < 0.5 then
- return (1 - math.sqrt(1 - 4 * t * t)) / 2
- else
- local t1 = -2 * t + 2
- return (math.sqrt(1 - t1 * t1) + 1) / 2
- end
-end
-
----@type EasingFunction
-function Easing.easeInBack(t)
- local c1 = 1.70158
- local c3 = c1 + 1
- return c3 * t * t * t - c1 * t * t
-end
-
----@type EasingFunction
-function Easing.easeOutBack(t)
- local c1 = 1.70158
- local c3 = c1 + 1
- local t1 = t - 1
- return 1 + c3 * t1 * t1 * t1 + c1 * t1 * t1
-end
-
----@type EasingFunction
-function Easing.easeInOutBack(t)
- local c1 = 1.70158
- local c2 = c1 * 1.525
-
- if t < 0.5 then
- return (2 * t * 2 * t * ((c2 + 1) * 2 * t - c2)) / 2
- else
- local t1 = 2 * t - 2
- return (t1 * t1 * ((c2 + 1) * t1 + c2) + 2) / 2
- end
-end
-
----@type EasingFunction
-function Easing.easeInElastic(t)
- if t == 0 then
- return 0
- end
- if t == 1 then
- return 1
- end
-
- local c4 = (2 * math.pi) / 3
- return -math.pow(2, 10 * t - 10) * math.sin((t * 10 - 10.75) * c4)
-end
-
----@type EasingFunction
-function Easing.easeOutElastic(t)
- if t == 0 then
- return 0
- end
- if t == 1 then
- return 1
- end
-
- local c4 = (2 * math.pi) / 3
- return math.pow(2, -10 * t) * math.sin((t * 10 - 0.75) * c4) + 1
-end
-
----@type EasingFunction
-function Easing.easeInOutElastic(t)
- if t == 0 then
- return 0
- end
- if t == 1 then
- return 1
- end
-
- local c5 = (2 * math.pi) / 4.5
-
- if t < 0.5 then
- return -(math.pow(2, 20 * t - 10) * math.sin((20 * t - 11.125) * c5)) / 2
- else
- return (math.pow(2, -20 * t + 10) * math.sin((20 * t - 11.125) * c5)) / 2 + 1
- end
-end
-
----@type EasingFunction
-function Easing.easeOutBounce(t)
- local n1 = 7.5625
- local d1 = 2.75
-
- if t < 1 / d1 then
- return n1 * t * t
- elseif t < 2 / d1 then
- local t1 = t - 1.5 / d1
- return n1 * t1 * t1 + 0.75
- elseif t < 2.5 / d1 then
- local t1 = t - 2.25 / d1
- return n1 * t1 * t1 + 0.9375
- else
- local t1 = t - 2.625 / d1
- return n1 * t1 * t1 + 0.984375
- end
-end
-
----@type EasingFunction
-function Easing.easeInBounce(t)
- return 1 - Easing.easeOutBounce(1 - t)
-end
-
----@type EasingFunction
-function Easing.easeInOutBounce(t)
- if t < 0.5 then
- return (1 - Easing.easeOutBounce(1 - 2 * t)) / 2
- else
- return (1 + Easing.easeOutBounce(2 * t - 1)) / 2
- end
-end
-
---- Create a custom back easing function with configurable overshoot
----@param overshoot number? Overshoot amount (default: 1.70158)
----@return EasingFunction
-function Easing.back(overshoot)
- overshoot = overshoot or 1.70158
- local c3 = overshoot + 1
-
- return function(t)
- return c3 * t * t * t - overshoot * t * t
- end
-end
-
---- Create a custom elastic easing function
----@param amplitude number? Amplitude (default: 1)
----@param period number? Period (default: 0.3)
----@return EasingFunction
-function Easing.elastic(amplitude, period)
- amplitude = amplitude or 1
- period = period or 0.3
-
- return function(t)
- if t == 0 then
- return 0
- end
- if t == 1 then
- return 1
- end
-
- local s = period / 4
- local a = amplitude
-
- if a < 1 then
- a = 1
- s = period / 4
- else
- s = period / (2 * math.pi) * math.asin(1 / a)
- end
-
- return a * math.pow(2, -10 * t) * math.sin((t - s) * (2 * math.pi) / period) + 1
- end
-end
-
--- ============================================================================
--- TRANSFORM
--- ============================================================================
-
-local Transform = {}
-Transform.__index = Transform
-
---- Create a new transform instance
----@param props Transform?
----@return Transform transform
-function Transform.new(props)
- props = props or {}
-
- local self = setmetatable({}, Transform)
-
- self.rotate = props.rotate or 0
- self.scaleX = props.scaleX or 1
- self.scaleY = props.scaleY or 1
- self.translateX = props.translateX or 0
- self.translateY = props.translateY or 0
- self.skewX = props.skewX or 0
- self.skewY = props.skewY or 0
- self.originX = props.originX or 0.5
- self.originY = props.originY or 0.5
-
- return self
-end
-
---- Apply transform to LÖVE graphics context
----@param transform Transform Transform instance
----@param x number Element x position
----@param y number Element y position
----@param width number Element width
----@param height number Element height
-function Transform.apply(transform, x, y, width, height)
- if not transform then
- return
- end
-
- local ox = x + width * transform.originX
- local oy = y + height * transform.originY
-
- love.graphics.push()
- love.graphics.translate(ox, oy)
-
- if transform.rotate ~= 0 then
- love.graphics.rotate(transform.rotate)
- end
-
- if transform.scaleX ~= 1 or transform.scaleY ~= 1 then
- love.graphics.scale(transform.scaleX, transform.scaleY)
- end
-
- if transform.skewX ~= 0 or transform.skewY ~= 0 then
- love.graphics.shear(transform.skewX, transform.skewY)
- end
-
- love.graphics.translate(-ox, -oy)
- love.graphics.translate(transform.translateX, transform.translateY)
-end
-
---- Remove transform from LÖVE graphics context
-function Transform.unapply()
- love.graphics.pop()
-end
-
---- Interpolate between two transforms
----@param from Transform Starting transform
----@param to Transform Ending transform
----@param t number Interpolation factor (0-1)
----@return Transform interpolated
-function Transform.lerp(from, to, t)
- if type(from) ~= "table" then
- from = Transform.new()
- end
- if type(to) ~= "table" then
- to = Transform.new()
- end
- if type(t) ~= "number" or t ~= t then
- t = 0
- elseif t == math.huge then
- t = 1
- elseif t == -math.huge then
- t = 0
- else
- t = math.max(0, math.min(1, t))
- end
-
- return Transform.new({
- rotate = (from.rotate or 0) * (1 - t) + (to.rotate or 0) * t,
- scaleX = (from.scaleX or 1) * (1 - t) + (to.scaleX or 1) * t,
- scaleY = (from.scaleY or 1) * (1 - t) + (to.scaleY or 1) * t,
- translateX = (from.translateX or 0) * (1 - t) + (to.translateX or 0) * t,
- translateY = (from.translateY or 0) * (1 - t) + (to.translateY or 0) * t,
- skewX = (from.skewX or 0) * (1 - t) + (to.skewX or 0) * t,
- skewY = (from.skewY or 0) * (1 - t) + (to.skewY or 0) * t,
- originX = (from.originX or 0.5) * (1 - t) + (to.originX or 0.5) * t,
- originY = (from.originY or 0.5) * (1 - t) + (to.originY or 0.5) * t,
- })
-end
-
---- Check if transform is identity (no transformation)
----@param transform Transform
----@return boolean isIdentity
-function Transform.isIdentity(transform)
- if not transform then
- return true
- end
-
- return transform.rotate == 0
- and transform.scaleX == 1
- and transform.scaleY == 1
- and transform.translateX == 0
- and transform.translateY == 0
- and transform.skewX == 0
- and transform.skewY == 0
-end
-
---- Clone a transform
----@param transform Transform
----@return Transform clone
-function Transform.clone(transform)
- if not transform then
- return Transform.new()
- end
-
- return Transform.new({
- rotate = transform.rotate,
- scaleX = transform.scaleX,
- scaleY = transform.scaleY,
- translateX = transform.translateX,
- translateY = transform.translateY,
- skewX = transform.skewX,
- skewY = transform.skewY,
- originX = transform.originX,
- originY = transform.originY,
- })
-end
-
--- ============================================================================
--- INTERPOLATION HELPERS
--- ============================================================================
-
---- Helper function to interpolate numeric values
----@param startValue number Starting value
----@param finalValue number Final value
----@param easedT number Eased time (0-1)
----@return number interpolated Interpolated value
-local function lerpNumber(startValue, finalValue, easedT)
- return startValue * (1 - easedT) + finalValue * easedT
-end
-
---- Helper function to interpolate Color values
----@param startColor any Starting color (Color instance or parseable color)
----@param finalColor any Final color (Color instance or parseable color)
----@param easedT number Eased time (0-1)
----@param ColorModule table Color module reference
----@return any interpolated Interpolated Color instance
-local function lerpColor(startColor, finalColor, easedT, ColorModule)
- if not ColorModule or not ColorModule.parse or not ColorModule.lerp then
- return startColor
- end
-
- local colorA = ColorModule.parse(startColor)
- local colorB = ColorModule.parse(finalColor)
-
- return ColorModule.lerp(colorA, colorB, easedT)
-end
-
---- Helper function to interpolate table values (padding, margin, cornerRadius)
----@param startTable table Starting table
----@param finalTable table Final table
----@param easedT number Eased time (0-1)
----@return table interpolated Interpolated table
-local function lerpTable(startTable, finalTable, easedT)
- local result = {}
-
- local keys = {}
- for k in pairs(startTable) do
- keys[k] = true
- end
- for k in pairs(finalTable) do
- keys[k] = true
- end
-
- for key in pairs(keys) do
- local startVal = startTable[key]
- local finalVal = finalTable[key]
-
- if type(startVal) == "number" and type(finalVal) == "number" then
- result[key] = lerpNumber(startVal, finalVal, easedT)
- elseif startVal ~= nil then
- result[key] = startVal
- else
- result[key] = finalVal
- end
- end
-
- return result
-end
-
----@class Animation
-local Animation = {
- _Transform = Transform,
-}
-Animation.__index = Animation
-
---- Build smooth, timed transitions between visual states
----@param props AnimationProps Animation properties
----@return Animation animation The new animation instance
-function Animation.new(props)
- if type(props) ~= "table" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_001")
- end
- props = { duration = 1, start = {}, final = {} }
- end
-
- if type(props.duration) ~= "number" or props.duration <= 0 then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_002")
- end
- props.duration = 1
- end
-
- if type(props.start) ~= "table" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_001")
- end
- props.start = {}
- end
-
- if type(props.final) ~= "table" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_001")
- end
- props.final = {}
- end
-
- local self = setmetatable({}, Animation)
- self.duration = props.duration
- self.start = props.start
- self.final = props.final
- self.keyframes = props.keyframes
- self.transform = props.transform
- self.transition = props.transition
- self.elapsed = 0
-
- self.onStart = props.onStart
- self.onUpdate = props.onUpdate
- self.onComplete = props.onComplete
- self.onCancel = props.onCancel
- self._hasStarted = false
-
- self._paused = false
- self._reversed = false
- self._speed = 1.0
- self._state = "pending"
-
- local easingName = props.easing or "linear"
- if type(easingName) == "string" then
- self.easing = Easing[easingName] or Easing.linear
- elseif type(easingName) == "function" then
- self.easing = easingName
- else
- self.easing = Easing.linear
- end
-
- self._cachedResult = {}
- self._resultDirty = true
-
- return self
-end
-
---- Advance the animation timeline
----@param dt number Delta time in seconds
----@param element table? Optional element reference for callbacks
----@return boolean completed True if animation is complete
-function Animation:update(dt, element)
- if type(dt) ~= "number" or dt < 0 or dt ~= dt or dt == math.huge then
- dt = 0
- end
-
- if self._paused then
- return false
- end
-
- if self._delay and self._delayElapsed then
- if self._delayElapsed < self._delay then
- self._delayElapsed = self._delayElapsed + dt
- return false
- end
- end
-
- if not self._hasStarted then
- self._hasStarted = true
- self._state = "playing"
- if self.onStart and type(self.onStart) == "function" then
- local success, err = pcall(self.onStart, self, element)
- if not success then
- Animation._ErrorHandler:warn("Animation", "EVT_002", {
- callback = "onStart",
- error = tostring(err),
- })
- end
- end
- end
-
- dt = dt * self._speed
-
- if self._reversed then
- self.elapsed = self.elapsed - dt
- if self.elapsed <= 0 then
- self.elapsed = 0
- self._state = "completed"
- self._resultDirty = true
- if self.onComplete and type(self.onComplete) == "function" then
- local success, err = pcall(self.onComplete, self, element)
- if not success then
- Animation._ErrorHandler:warn("Animation", "EVT_002", {
- callback = "onComplete",
- error = tostring(err),
- })
- end
- end
- return true
- end
- else
- self.elapsed = self.elapsed + dt
- if self.elapsed >= self.duration then
- self.elapsed = self.duration
- self._resultDirty = true
-
- if self._repeatCount then
- self._repeatCurrent = (self._repeatCurrent or 0) + 1
-
- if self._repeatCount == 0 or self._repeatCurrent < self._repeatCount then
- if self._yoyo then
- self._reversed = not self._reversed
- if self._reversed then
- self.elapsed = self.duration
- else
- self.elapsed = 0
- end
- else
- self.elapsed = 0
- end
- return false
- end
- end
-
- self._state = "completed"
- if self.onComplete and type(self.onComplete) == "function" then
- local success, err = pcall(self.onComplete, self, element)
- if not success then
- Animation._ErrorHandler:warn("Animation", "EVT_002", {
- callback = "onComplete",
- error = tostring(err),
- })
- end
- end
- return true
- end
- end
-
- self._resultDirty = true
-
- if self.onUpdate and type(self.onUpdate) == "function" then
- local progress = self.elapsed / self.duration
- local success, err = pcall(self.onUpdate, self, element, progress)
- if not success then
- Animation._ErrorHandler:warn("Animation", "EVT_002", {
- callback = "onUpdate",
- error = tostring(err),
- })
- end
- end
-
- return false
-end
-
---- Find the two keyframes surrounding the current progress
----@param progress number Current animation progress (0-1)
----@return Keyframe? prevFrame The keyframe before current progress
----@return Keyframe? nextFrame The keyframe after current progress
-function Animation:findKeyframes(progress)
- if not self.keyframes or #self.keyframes < 2 then
- return nil, nil
- end
-
- local prevFrame = self.keyframes[1]
- local nextFrame = self.keyframes[#self.keyframes]
-
- for i = 1, #self.keyframes - 1 do
- if progress >= self.keyframes[i].at and progress <= self.keyframes[i + 1].at then
- prevFrame = self.keyframes[i]
- nextFrame = self.keyframes[i + 1]
- break
- end
- end
-
- return prevFrame, nextFrame
-end
-
---- Interpolate between two keyframes
----@param prevFrame Keyframe Starting keyframe
----@param nextFrame Keyframe Ending keyframe
----@param easedT number Eased time (0-1) for interpolation
----@return table result Interpolated values
-function Animation:lerpKeyframes(prevFrame, nextFrame, easedT)
- local result = {}
-
- local keys = {}
- for k in pairs(prevFrame.values) do
- keys[k] = true
- end
- for k in pairs(nextFrame.values) do
- keys[k] = true
- end
-
- local numericSet = {
- width = true,
- height = true,
- opacity = true,
- x = true,
- y = true,
- gap = true,
- imageOpacity = true,
- scrollbarWidth = true,
- borderWidth = true,
- fontSize = true,
- lineHeight = true,
- }
-
- local colorSet = {
- backgroundColor = true,
- borderColor = true,
- textColor = true,
- scrollbarColor = true,
- scrollbarBackgroundColor = true,
- imageTint = true,
- }
-
- local tableSet = {
- padding = true,
- margin = true,
- cornerRadius = true,
- }
-
- for key in pairs(keys) do
- local startVal = prevFrame.values[key]
- local finalVal = nextFrame.values[key]
-
- if numericSet[key] and type(startVal) == "number" and type(finalVal) == "number" then
- result[key] = lerpNumber(startVal, finalVal, easedT)
- elseif colorSet[key] and Animation._ColorModule then
- if startVal ~= nil and finalVal ~= nil then
- result[key] = lerpColor(startVal, finalVal, easedT, Animation._ColorModule)
- end
- elseif tableSet[key] and type(startVal) == "table" and type(finalVal) == "table" then
- result[key] = lerpTable(startVal, finalVal, easedT)
- elseif type(startVal) == type(finalVal) then
- if type(startVal) == "number" then
- result[key] = lerpNumber(startVal, finalVal, easedT)
- else
- result[key] = finalVal
- end
- end
- end
-
- return result
-end
-
---- Calculate the current animated values
----@return table result Interpolated values
-function Animation:interpolate()
- if not self._resultDirty then
- return self._cachedResult
- end
-
- local t = math.min(self.elapsed / self.duration, 1)
-
- if self.keyframes and type(self.keyframes) == "table" and #self.keyframes >= 2 then
- local prevFrame, nextFrame = self:findKeyframes(t)
-
- if prevFrame and nextFrame then
- local localProgress = 0
- if nextFrame.at > prevFrame.at then
- localProgress = (t - prevFrame.at) / (nextFrame.at - prevFrame.at)
- end
-
- local easingFn = Easing.linear
- if prevFrame.easing then
- if type(prevFrame.easing) == "string" then
- easingFn = Easing[prevFrame.easing] or Easing.linear
- elseif type(prevFrame.easing) == "function" then
- easingFn = prevFrame.easing
- end
- end
-
- local success, easedT = pcall(easingFn, localProgress)
- if not success or type(easedT) ~= "number" or easedT ~= easedT or easedT == math.huge or easedT == -math.huge then
- easedT = localProgress
- end
-
- local keyframeResult = self:lerpKeyframes(prevFrame, nextFrame, easedT)
-
- local result = self._cachedResult
- for k in pairs(result) do
- result[k] = nil
- end
- for k, v in pairs(keyframeResult) do
- result[k] = v
- end
-
- self._resultDirty = false
- return result
- end
- end
-
- local success, easedT = pcall(self.easing, t)
- if not success or type(easedT) ~= "number" or easedT ~= easedT or easedT == math.huge or easedT == -math.huge then
- easedT = t
- end
-
- local result = self._cachedResult
-
- for k in pairs(result) do
- result[k] = nil
- end
-
- local numericProperties = {
- "width",
- "height",
- "opacity",
- "x",
- "y",
- "gap",
- "imageOpacity",
- "scrollbarWidth",
- "borderWidth",
- "fontSize",
- "lineHeight",
- }
-
- local colorProperties = {
- "backgroundColor",
- "borderColor",
- "textColor",
- "scrollbarColor",
- "scrollbarBackgroundColor",
- "imageTint",
- }
-
- local tableProperties = {
- "padding",
- "margin",
- "cornerRadius",
- }
-
- for _, prop in ipairs(numericProperties) do
- local startVal = self.start[prop]
- local finalVal = self.final[prop]
-
- if type(startVal) == "number" and type(finalVal) == "number" then
- result[prop] = lerpNumber(startVal, finalVal, easedT)
- end
- end
-
- if Animation._ColorModule then
- for _, prop in ipairs(colorProperties) do
- local startVal = self.start[prop]
- local finalVal = self.final[prop]
-
- if startVal ~= nil and finalVal ~= nil then
- result[prop] = lerpColor(startVal, finalVal, easedT, Animation._ColorModule)
- end
- end
- end
-
- for _, prop in ipairs(tableProperties) do
- local startVal = self.start[prop]
- local finalVal = self.final[prop]
-
- if type(startVal) == "table" and type(finalVal) == "table" then
- result[prop] = lerpTable(startVal, finalVal, easedT)
- end
- end
-
- if Animation._Transform and self.start.transform and self.final.transform then
- result.transform = Animation._Transform.lerp(self.start.transform, self.final.transform, easedT)
- end
-
- if self.transform and type(self.transform) == "table" then
- for key, value in pairs(self.transform) do
- result[key] = value
- end
- end
-
- self._resultDirty = false
- return result
-end
-
---- Attach animation to an element
----@param element table The element to apply animation to
-function Animation:apply(element)
- if not element or type(element) ~= "table" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_003")
- end
- return
- end
- element.animation = self
-end
-
---- Apply interpolated values to an element during update.
---- Called each frame while the animation is active (not yet finished).
----@param element table Element to apply interpolated properties to
-function Animation:applyInterpolation(element)
- local anim = self:interpolate()
-
- -- Numeric properties
- element.width = anim.width or element.width
- element.height = anim.height or element.height
- element.opacity = anim.opacity or element.opacity
- element.x = anim.x or element.x
- element.y = anim.y or element.y
- element.gap = anim.gap or element.gap
- element.imageOpacity = anim.imageOpacity or element.imageOpacity
- element.scrollbarWidth = anim.scrollbarWidth or element.scrollbarWidth
- element.borderWidth = anim.borderWidth or element.borderWidth
- element.fontSize = anim.fontSize or element.fontSize
- element.lineHeight = anim.lineHeight or element.lineHeight
-
- -- Color properties
- if anim.backgroundColor then
- element.backgroundColor = anim.backgroundColor
- end
- if anim.borderColor then
- element.borderColor = anim.borderColor
- end
- if anim.textColor then
- element.textColor = anim.textColor
- end
- if anim.scrollbarColor then
- element.scrollbarColor = anim.scrollbarColor
- end
- if anim.scrollbarBackgroundColor then
- element.scrollbarBackgroundColor = anim.scrollbarBackgroundColor
- end
- if anim.imageTint then
- element.imageTint = anim.imageTint
- end
-
- -- Table properties
- if anim.padding then
- element.padding = anim.padding
- end
- if anim.margin then
- element.margin = anim.margin
- end
- if anim.cornerRadius then
- element.cornerRadius = anim.cornerRadius
- end
- if anim.transform then
- element.transform = anim.transform
- end
-
- -- Backward compatibility: opacity-only animation updates background alpha
- if anim.opacity and not anim.backgroundColor then
- element.backgroundColor.a = anim.opacity
- end
-end
-
---- Pause animation
-function Animation:pause()
- if self._state == "playing" or self._state == "pending" then
- self._paused = true
- self._state = "paused"
- end
-end
-
---- Resume animation
-function Animation:resume()
- if self._state == "paused" then
- self._paused = false
- self._state = "playing"
- end
-end
-
---- Check if paused
----@return boolean paused
-function Animation:isPaused()
- return self._paused
-end
-
---- Reverse animation direction
-function Animation:reverse()
- self._reversed = not self._reversed
-end
-
---- Check if reversed
----@return boolean reversed
-function Animation:isReversed()
- return self._reversed
-end
-
---- Set playback speed
----@param speed number Speed multiplier
-function Animation:setSpeed(speed)
- if type(speed) == "number" and speed > 0 then
- self._speed = speed
- end
-end
-
---- Get playback speed
----@return number speed
-function Animation:getSpeed()
- return self._speed
-end
-
---- Seek to specific time
----@param time number Time in seconds
-function Animation:seek(time)
- if type(time) == "number" then
- self.elapsed = math.max(0, math.min(time, self.duration))
- self._resultDirty = true
- end
-end
-
---- Get animation state
----@return string state
-function Animation:getState()
- return self._state
-end
-
---- Cancel animation
----@param element table? Optional element reference
-function Animation:cancel(element)
- if self._state ~= "cancelled" and self._state ~= "completed" then
- self._state = "cancelled"
- if self.onCancel and type(self.onCancel) == "function" then
- local success, err = pcall(self.onCancel, self, element)
- if not success then
- Animation._ErrorHandler:warn("Animation", "EVT_002", {
- callback = "onCancel",
- error = tostring(err),
- })
- end
- end
- end
-end
-
---- Reset animation
-function Animation:reset()
- self.elapsed = 0
- self._hasStarted = false
- self._paused = false
- self._state = "pending"
- self._resultDirty = true
-end
-
---- Get animation progress
----@return number progress
-function Animation:getProgress()
- return math.min(self.elapsed / self.duration, 1)
-end
-
---- Chain animations
----@param nextAnimation Animation|function
----@return Animation nextAnimation
-function Animation:chain(nextAnimation)
- if type(nextAnimation) == "function" then
- self._nextFactory = nextAnimation
- return self
- elseif type(nextAnimation) == "table" then
- self._next = nextAnimation
- return nextAnimation
- else
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_004")
- end
- return self
- end
-end
-
---- Add delay before animation starts
----@param seconds number Delay duration
----@return Animation self
-function Animation:delay(seconds)
- if type(seconds) ~= "number" or seconds < 0 then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_005")
- end
- seconds = 0
- end
- self._delay = seconds
- self._delayElapsed = 0
- return self
-end
-
---- Set repeat count
----@param count number Repeat count (0 = infinite)
----@return Animation self
-function Animation:repeatCount(count)
- if type(count) ~= "number" or count < 0 then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_006")
- end
- count = 0
- end
- self._repeatCount = count
- self._repeatCurrent = 0
- return self
-end
-
---- Enable yoyo mode
----@param enabled boolean? Enable yoyo (default: true)
----@return Animation self
-function Animation:yoyo(enabled)
- if enabled == nil then
- enabled = true
- end
- self._yoyo = enabled
- return self
-end
-
---- Create fade animation
----@param duration number Duration in seconds
----@param fromOpacity number Starting opacity
----@param toOpacity number Ending opacity
----@param easing string? Easing function name
----@return Animation animation
-function Animation.fade(duration, fromOpacity, toOpacity, easing)
- if type(duration) ~= "number" or duration <= 0 then
- duration = 1
- end
- if type(fromOpacity) ~= "number" then
- fromOpacity = 1
- end
- if type(toOpacity) ~= "number" then
- toOpacity = 0
- end
-
- return Animation.new({
- duration = duration,
- start = { opacity = fromOpacity },
- final = { opacity = toOpacity },
- easing = easing,
- })
-end
-
---- Create scale animation
----@param duration number Duration in seconds
----@param fromScale {width:number,height:number} Starting scale
----@param toScale {width:number,height:number} Ending scale
----@param easing string? Easing function name
----@return Animation animation
-function Animation.scale(duration, fromScale, toScale, easing)
- if type(duration) ~= "number" or duration <= 0 then
- duration = 1
- end
- if type(fromScale) ~= "table" then
- fromScale = { width = 1, height = 1 }
- end
- if type(toScale) ~= "table" then
- toScale = { width = 1, height = 1 }
- end
-
- return Animation.new({
- duration = duration,
- start = { width = fromScale.width or 0, height = fromScale.height or 0 },
- final = { width = toScale.width or 0, height = toScale.height or 0 },
- easing = easing,
- })
-end
-
---- Create keyframe animation
----@param props {duration:number, keyframes:Keyframe[], onStart:function?, onUpdate:function?, onComplete:function?, onCancel:function?}
----@return Animation animation
-function Animation.keyframes(props)
- if type(props) ~= "table" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_007")
- end
- props = { duration = 1, keyframes = {} }
- end
-
- if type(props.duration) ~= "number" or props.duration <= 0 then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_002")
- end
- props.duration = 1
- end
-
- if type(props.keyframes) ~= "table" or #props.keyframes < 2 then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_008")
- end
- props.keyframes = {
- { at = 0, values = {} },
- { at = 1, values = {} },
- }
- end
-
- local sortedKeyframes = {}
- for i, kf in ipairs(props.keyframes) do
- if type(kf) == "table" and type(kf.at) == "number" and type(kf.values) == "table" then
- table.insert(sortedKeyframes, kf)
- end
- end
-
- table.sort(sortedKeyframes, function(a, b)
- return a.at < b.at
- end)
-
- if #sortedKeyframes > 0 then
- if sortedKeyframes[1].at > 0 then
- table.insert(sortedKeyframes, 1, { at = 0, values = sortedKeyframes[1].values })
- end
- if sortedKeyframes[#sortedKeyframes].at < 1 then
- table.insert(sortedKeyframes, { at = 1, values = sortedKeyframes[#sortedKeyframes].values })
- end
- end
-
- return Animation.new({
- duration = props.duration,
- start = {},
- final = {},
- keyframes = sortedKeyframes,
- onStart = props.onStart,
- onUpdate = props.onUpdate,
- onComplete = props.onComplete,
- onCancel = props.onCancel,
- })
-end
-
---- Link an array of animations into a chain (static helper)
---- Each animation's completion triggers the next in sequence
----@param animations Animation[] Array of animations to chain
----@return Animation first The first animation in the chain
-function Animation.chainSequence(animations)
- if type(animations) ~= "table" or #animations == 0 then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("Animation", "ANIM_004")
- end
- return Animation.new({ duration = 0, start = {}, final = {} })
- end
-
- for i = 1, #animations - 1 do
- animations[i]:chain(animations[i + 1])
- end
-
- return animations[1]
-end
-
--- ============================================================================
--- ANIMATION GROUP (Utility)
--- ============================================================================
-
-local AnimationGroup = {}
-AnimationGroup.__index = AnimationGroup
-
---- Coordinate multiple animations
----@param props AnimationGroupProps
----@return AnimationGroup group
-function AnimationGroup.new(props)
- if type(props) ~= "table" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("AnimationGroup", "ANIM_009")
- end
- props = { animations = {} }
- end
-
- if type(props.animations) ~= "table" or #props.animations == 0 then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("AnimationGroup", "ANIM_010")
- end
- props.animations = {}
- end
-
- local self = setmetatable({}, AnimationGroup)
-
- self.animations = props.animations
- self.mode = props.mode or "parallel"
- self.stagger = props.stagger or 0.1
- self.onComplete = props.onComplete
- self.onStart = props.onStart
-
- if self.mode ~= "parallel" and self.mode ~= "sequence" and self.mode ~= "stagger" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("AnimationGroup", "ANIM_011", {
- mode = tostring(self.mode),
- })
- end
- self.mode = "parallel"
- end
-
- self._currentIndex = 1
- self._staggerElapsed = 0
- self._startedAnimations = {}
- self._hasStarted = false
- self._paused = false
- self._state = "ready"
-
- return self
-end
-
---- Update all animations in parallel
----@param dt number Delta time
----@param element table? Optional element reference
----@return boolean finished
-function AnimationGroup:_updateParallel(dt, element)
- local allFinished = true
-
- for i, anim in ipairs(self.animations) do
- local isCompleted = false
- if type(anim.getState) == "function" then
- isCompleted = anim:getState() == "completed"
- elseif anim._state then
- isCompleted = anim._state == "completed"
- end
-
- if not isCompleted then
- local finished = anim:update(dt, element)
- if not finished then
- allFinished = false
- end
- end
- end
-
- return allFinished
-end
-
---- Update animations in sequence
----@param dt number Delta time
----@param element table? Optional element reference
----@return boolean finished
-function AnimationGroup:_updateSequence(dt, element)
- if self._currentIndex > #self.animations then
- return true
- end
-
- local currentAnim = self.animations[self._currentIndex]
- local finished = currentAnim:update(dt, element)
-
- if finished then
- self._currentIndex = self._currentIndex + 1
- if self._currentIndex > #self.animations then
- return true
- end
- end
-
- return false
-end
-
---- Update animations with stagger
----@param dt number Delta time
----@param element table? Optional element reference
----@return boolean finished
-function AnimationGroup:_updateStagger(dt, element)
- self._staggerElapsed = self._staggerElapsed + dt
-
- for i, anim in ipairs(self.animations) do
- local startTime = (i - 1) * self.stagger
-
- if self._staggerElapsed >= startTime and not self._startedAnimations[i] then
- self._startedAnimations[i] = true
- end
- end
-
- local allFinished = true
- for i, anim in ipairs(self.animations) do
- if self._startedAnimations[i] then
- local isCompleted = false
- if type(anim.getState) == "function" then
- isCompleted = anim:getState() == "completed"
- elseif anim._state then
- isCompleted = anim._state == "completed"
- end
-
- if not isCompleted then
- local finished = anim:update(dt, element)
- if not finished then
- allFinished = false
- end
- end
- else
- allFinished = false
- end
- end
-
- return allFinished
-end
-
---- Advance all animations in the group
----@param dt number Delta time
----@param element table? Optional element reference
----@return boolean finished
-function AnimationGroup:update(dt, element)
- if type(dt) ~= "number" or dt < 0 or dt ~= dt or dt == math.huge then
- dt = 0
- end
-
- if self._paused or self._state == "completed" or self._state == "cancelled" then
- return self._state == "completed"
- end
-
- if not self._hasStarted then
- self._hasStarted = true
- self._state = "playing"
- if self.onStart and type(self.onStart) == "function" then
- local success, err = pcall(self.onStart, self)
- if not success then
- Animation._ErrorHandler:warn("Animation", "EVT_002", {
- callback = "onStart",
- error = tostring(err),
- })
- end
- end
- end
-
- local finished = false
-
- if self.mode == "parallel" then
- finished = self:_updateParallel(dt, element)
- elseif self.mode == "sequence" then
- finished = self:_updateSequence(dt, element)
- elseif self.mode == "stagger" then
- finished = self:_updateStagger(dt, element)
- end
-
- if finished then
- self._state = "completed"
- if self.onComplete and type(self.onComplete) == "function" then
- local success, err = pcall(self.onComplete, self)
- if not success then
- Animation._ErrorHandler:warn("Animation", "EVT_002", {
- callback = "onComplete",
- error = tostring(err),
- })
- end
- end
- end
-
- return finished
-end
-
---- Pause all animations
-function AnimationGroup:pause()
- self._paused = true
- for _, anim in ipairs(self.animations) do
- if type(anim.pause) == "function" then
- anim:pause()
- end
- end
-end
-
---- Resume all animations
-function AnimationGroup:resume()
- self._paused = false
- for _, anim in ipairs(self.animations) do
- if type(anim.resume) == "function" then
- anim:resume()
- end
- end
-end
-
---- Check if paused
----@return boolean paused
-function AnimationGroup:isPaused()
- return self._paused
-end
-
---- Reverse all animations
-function AnimationGroup:reverse()
- for _, anim in ipairs(self.animations) do
- if type(anim.reverse) == "function" then
- anim:reverse()
- end
- end
-end
-
---- Set speed for all animations
----@param speed number Speed multiplier
-function AnimationGroup:setSpeed(speed)
- for _, anim in ipairs(self.animations) do
- if type(anim.setSpeed) == "function" then
- anim:setSpeed(speed)
- end
- end
-end
-
---- Cancel all animations
----@param element table? Optional element reference
-function AnimationGroup:cancel(element)
- if self._state ~= "cancelled" and self._state ~= "completed" then
- self._state = "cancelled"
- for _, anim in ipairs(self.animations) do
- if type(anim.cancel) == "function" then
- anim:cancel(element)
- end
- end
- end
-end
-
---- Reset all animations
-function AnimationGroup:reset()
- self._currentIndex = 1
- self._staggerElapsed = 0
- self._startedAnimations = {}
- self._hasStarted = false
- self._paused = false
- self._state = "ready"
-
- for _, anim in ipairs(self.animations) do
- if type(anim.reset) == "function" then
- anim:reset()
- end
- end
-end
-
---- Get group state
----@return string state
-function AnimationGroup:getState()
- return self._state
-end
-
---- Get group progress
----@return number progress
-function AnimationGroup:getProgress()
- if #self.animations == 0 then
- return 1
- end
-
- if self.mode == "sequence" then
- local completedAnims = self._currentIndex - 1
- local currentProgress = 0
-
- if self._currentIndex <= #self.animations then
- local currentAnim = self.animations[self._currentIndex]
- if type(currentAnim.getProgress) == "function" then
- currentProgress = currentAnim:getProgress()
- end
- end
-
- return (completedAnims + currentProgress) / #self.animations
- else
- local totalProgress = 0
- for _, anim in ipairs(self.animations) do
- if type(anim.getProgress) == "function" then
- totalProgress = totalProgress + anim:getProgress()
- else
- totalProgress = totalProgress + 1
- end
- end
- return totalProgress / #self.animations
- end
-end
-
---- Apply group to element
----@param element table The element to apply animations to
-function AnimationGroup:apply(element)
- if not element or type(element) ~= "table" then
- if Animation._ErrorHandler then
- Animation._ErrorHandler:warn("AnimationGroup", "ANIM_003")
- end
- return
- end
- element.animationGroup = self
-end
-
--- ============================================================================
--- MODULE INITIALIZATION
--- ============================================================================
-
---- Initialize Animation module with dependencies
----@param deps table Dependencies: { ErrorHandler = ErrorHandler, Color = Color? }
-function Animation.init(deps)
- if type(deps) == "table" then
- Animation._ErrorHandler = deps.ErrorHandler
- Animation._ColorModule = deps.Color
- end
-end
-
-Animation.Easing = Easing
-Animation.Transform = Transform
-Animation.Group = AnimationGroup
-
-return Animation
diff --git a/libs/flexlove/modules/Behavior.lua b/libs/flexlove/modules/Behavior.lua
deleted file mode 100644
index c4c11b99..00000000
--- a/libs/flexlove/modules/Behavior.lua
+++ /dev/null
@@ -1,188 +0,0 @@
--- modules/Behavior.lua
---
--- Base module for the pluggable behavior system that drives the Behavior &
--- Mode Unification refactor.
---
--- A *behavior* is a small, stateless table produced by `Behavior.new(spec)`
--- that implements a fixed lifecycle hook set. Concrete behaviors (Clickable,
--- Scrollable, TextEditable, Selectable, ...) each live in their own module and
--- are attached to an Element. The Element's `update`/`draw`/save-restore paths
--- iterate `element.behaviors` and dispatch to the appropriate hooks, replacing
--- the swarm of `if self.scrollable` / immediate-mode-branch checks previously
--- hard-coded in Element.lua.
---
--- Element.new iterates a registry of behavior prototypes and auto-attaches
--- whichever return true from `shouldAttach(props)`. Element therefore never
--- needs to know what an individual behavior does — only that it conforms to
--- this interface.
---
--- Design constraints (locked — tasks 02-13 depend on this API):
--- * Pure Lua — NO `love` import, NO dependency on utils/Color/Units/ErrorHandler.
--- Stays fully stub-testable standalone (see testing/__tests__/behavior_test.lua).
--- * Minimal interface — exactly 6 lifecycle hooks + a `shouldAttach` predicate.
--- Do NOT add hooks "just in case"; new capabilities become new behaviors,
--- not new hooks. Extending HOOK_NAMES is an architectural decision that must
--- be mirrored by every concrete behavior.
--- * Immutable instances — behavior tables are produced once and treated as
--- read-only. Per-element runtime state lives on the element (or a subsystem
--- the behavior attaches), NEVER on the behavior instance itself, so a single
--- behavior instance can be shared across many elements.
---
--- Lifecycle hook contract (each receives the owning element as first argument):
--- onAttach(element) — called once when the behavior is attached
--- (element fully constructed). Allocate
--- subsystems / register listeners here.
--- onDetach(element) — called once when the behavior is detached
--- (element destroyed / mode switch). Tear
--- down anything onAttach created.
--- onUpdate(element, dt) — called every frame from Element:update.
--- onDraw(element, ctx) — called every frame from Element:draw; `ctx`
--- is the draw context (viewport transform,
--- scissor state, theme renderer, ...).
--- saveState(element) -> state — called during Element save-state; returns
--- a serializable snapshot (or nil) so the
--- behavior's runtime state survives the
--- immediate-mode recreation cycle.
--- restoreState(element, state) — called after reconstruction with the
--- snapshot previously returned by saveState.
---
--- shouldAttach(props) -> boolean — class-level predicate (not a hook): given
--- an element's props table, return true if
--- this behavior should be auto-attached.
--- Defaults to false (opt-in).
-
---- A behavior instance: a frozen table of lifecycle hooks + a shouldAttach
---- predicate. All hooks are always present (custom override or no-op default).
----@class Behavior
----@field onAttach fun(element:table)
----@field onDetach fun(element:table)
----@field onUpdate fun(element:table, dt:number)
----@field onDraw fun(element:table, ctx:table)
----@field saveState fun(element:table):any
----@field restoreState fun(element:table, state:any)
----@field shouldAttach fun(props:table):boolean
-
-local Behavior = {}
-
--- The fixed, ordered lifecycle hook set. Order is preserved so downstream tasks
--- (Element behavior iteration) can rely on a deterministic dispatch sequence.
--- HOOK_NAMES is intentionally NOT extended casually — see file header.
-Behavior.HOOK_NAMES = {
- "onAttach",
- "onDetach",
- "onUpdate",
- "onDraw",
- "saveState",
- "restoreState",
-}
-
--- Allowlist of spec keys accepted by Behavior.new. Anything else is rejected so
--- a typo (e.g. `onUpdat`) surfaces immediately instead of silently no-op'ing.
--- Hook keys (HOOK_NAMES + shouldAttach) MUST be functions; metadata keys
--- (drawLayer) may hold any value.
-local ALLOWED_KEYS = {
- onAttach = true,
- onDetach = true,
- onUpdate = true,
- onDraw = true,
- saveState = true,
- restoreState = true,
- shouldAttach = true,
- drawLayer = true,
-}
-
--- Spec keys whose values are NOT required to be functions (passive metadata
--- consumed by dispatch sites, e.g. Element:draw's pre/post-children split).
-local NON_FUNCTION_KEYS = {
- drawLayer = true,
-}
-
--- Default no-op hook. Behaviors override only the hooks they need; every other
--- hook resolves to this so dispatch sites never have to nil-check.
-local function noop() end
-
--- Default shouldAttach predicate: never auto-attach unless the behavior opts in
--- by providing its own predicate. This is the safe default — a behavior with no
--- opinion about which elements it applies to stays inert in the auto-attach
--- pass (it can still be attached explicitly by name in a later task).
-local function defaultShouldAttach()
- return false
-end
-
--- Module-level default predicate exposed for callers/tests that want to
--- reference the base default directly without constructing an instance.
-Behavior.shouldAttach = defaultShouldAttach
-
---- Factory: create a frozen behavior instance from a spec table.
----
---- `spec` is a table whose keys may be any subset of the 6 lifecycle hook names
---- plus `shouldAttach`; each value (when present) must be a function. The
---- returned table contains every lifecycle hook (custom override OR no-op) and
---- a `shouldAttach` predicate (custom OR always-false default), so dispatch
---- sites can call any hook unconditionally without nil-checking.
----
---- Unknown spec keys and non-function values raise an error immediately so
---- mistakes fail fast at construction rather than as silent no-ops later.
----
----@param spec table|nil spec table overriding select hooks / shouldAttach
----@return Behavior
-function Behavior.new(spec)
- spec = spec or {}
-
- -- Validate spec keys up front so typos surface here, not as silent no-ops.
- for key, value in pairs(spec) do
- if not ALLOWED_KEYS[key] then
- error(string.format("Behavior.new: unknown spec key '%s'", tostring(key)), 2)
- end
- if not NON_FUNCTION_KEYS[key] and type(value) ~= "function" then
- error(string.format("Behavior.new: spec key '%s' must be a function, got %s", tostring(key), type(value)), 2)
- end
- end
-
- local instance = {}
-
- -- Populate every lifecycle hook: custom override when provided, no-op default
- -- otherwise. Guarantees `instance.hook` is always callable.
- for _, hook in ipairs(Behavior.HOOK_NAMES) do
- instance[hook] = spec[hook] or noop
- end
-
- -- shouldAttach defaults to always-false; behaviors opt in by supplying one.
- instance.shouldAttach = spec.shouldAttach or defaultShouldAttach
-
- -- drawLayer: optional metadata field (default nil = "background"/pre-children).
- -- Dispatch sites (Element:draw) use it to split rendering into pre-children
- -- (background layers) and post-children (overlay layers, e.g. scrollbars).
- instance.drawLayer = spec.drawLayer
-
- -- Freeze: prevent adding new fields. Behavior instances are shared, stateless
- -- objects; runtime state belongs on the element, never on the behavior.
- -- (Reassigning an existing hook is still possible via direct index write —
- -- Lua metatables cannot intercept that — but the freeze communicates intent
- -- and catches accidental field additions.)
- local mt = {
- __newindex = function(_, key)
- error(string.format("Behavior: behavior instances are immutable (cannot set '%s')", tostring(key)), 2)
- end,
- --- Mark the metatable so consumers can detect a Behavior instance.
- ---@return string
- __tostring = function()
- return "Behavior"
- end,
- __metatable = "Behavior",
- }
- setmetatable(instance, mt)
-
- return instance
-end
-
---- Type guard: returns true if `value` is a Behavior instance produced by
---- `Behavior.new`. Used by Element's attach path to validate registry entries
---- without depending on identity.
----@param value any
----@return boolean
-function Behavior.isBehavior(value)
- return type(value) == "table" and getmetatable(value) == "Behavior"
-end
-
-return Behavior
diff --git a/libs/flexlove/modules/Blur.lua b/libs/flexlove/modules/Blur.lua
deleted file mode 100644
index 413592a9..00000000
--- a/libs/flexlove/modules/Blur.lua
+++ /dev/null
@@ -1,686 +0,0 @@
--- Lua 5.2+ compatibility for unpack
-local unpack = table.unpack or unpack
-
--- Warning cache to prevent duplicate warnings for the same element
-local warningCache = {}
-
-local Cache = {
- canvases = {},
- quads = {},
- blurInstances = {}, -- Cache blur instances by quality
- blurredCanvases = {}, -- Cache pre-blurred canvases for immediate mode
- MAX_CANVAS_SIZE = 20,
- MAX_QUAD_SIZE = 20,
- MAX_BLURRED_CANVAS_CACHE = 50, -- Maximum cached blurred canvases
- RADIUS_THRESHOLD = 0.5, -- Skip blur below this radius
- LARGE_BLUR_THRESHOLD = 250 * 250, -- Warn if blur area exceeds this (250x250px)
-}
-
---- Round canvas size to nearest bucket for better reuse
----@param size number Size to bucket
----@return number bucketSize Bucketed size
-local function bucketSize(size)
- if size <= 128 then
- return math.ceil(size / 32) * 32
- elseif size <= 512 then
- return math.ceil(size / 64) * 64
- elseif size <= 1024 then
- return math.ceil(size / 128) * 128
- else
- return math.ceil(size / 256) * 256
- end
-end
-
---- Get or create a canvas from cache
----@param width number Canvas width
----@param height number Canvas height
----@return love.Canvas canvas The cached or new canvas
-function Cache.getCanvas(width, height)
- -- Use bucketed sizes for better cache reuse
- local bucketedWidth = bucketSize(width)
- local bucketedHeight = bucketSize(height)
- local key = string.format("%dx%d", bucketedWidth, bucketedHeight)
-
- if not Cache.canvases[key] then
- Cache.canvases[key] = {}
- end
-
- local cache = Cache.canvases[key]
-
- for i, entry in ipairs(cache) do
- if not entry.inUse then
- entry.inUse = true
- return entry.canvas
- end
- end
-
- local canvas = love.graphics.newCanvas(bucketedWidth, bucketedHeight)
- table.insert(cache, { canvas = canvas, inUse = true })
-
- if #cache > Cache.MAX_CANVAS_SIZE then
- local removed = table.remove(cache, 1)
- if removed and removed.canvas then
- removed.canvas:release()
- end
- end
-
- return canvas
-end
-
---- Release a canvas back to the cache
----@param canvas love.Canvas Canvas to release
-function Cache.releaseCanvas(canvas)
- for _, sizeCache in pairs(Cache.canvases) do
- for _, entry in ipairs(sizeCache) do
- if entry.canvas == canvas then
- entry.inUse = false
- return
- end
- end
- end
-end
-
---- Get or create a quad from cache
----@param x number X position
----@param y number Y position
----@param width number Quad width
----@param height number Quad height
----@param sw number Source width
----@param sh number Source height
----@return love.Quad quad The cached or new quad
-function Cache.getQuad(x, y, width, height, sw, sh)
- local key = string.format("%d,%d,%d,%d,%d,%d", x, y, width, height, sw, sh)
-
- if not Cache.quads[key] then
- Cache.quads[key] = {}
- end
-
- local cache = Cache.quads[key]
-
- for i, entry in ipairs(cache) do
- if not entry.inUse then
- entry.inUse = true
- return entry.quad
- end
- end
-
- local quad = love.graphics.newQuad(x, y, width, height, sw, sh)
- table.insert(cache, { quad = quad, inUse = true })
-
- if #cache > Cache.MAX_QUAD_SIZE then
- table.remove(cache, 1)
- end
-
- return quad
-end
-
---- Release a quad back to the cache
----@param quad love.Quad Quad to release
-function Cache.releaseQuad(quad)
- for _, keyCache in pairs(Cache.quads) do
- for _, entry in ipairs(keyCache) do
- if entry.quad == quad then
- entry.inUse = false
- return
- end
- end
- end
-end
-
---- Generate cache key for blurred canvas
----@param elementId string Element ID
----@param x number X position
----@param y number Y position
----@param width number Width
----@param height number Height
----@param radius number Blur radius
----@param quality number Blur quality
----@param isBackdrop boolean Whether this is backdrop blur
----@return string key Cache key
-function Cache.generateBlurCacheKey(elementId, x, y, width, height, radius, quality, isBackdrop)
- return string.format(
- "%s:%d:%d:%d:%d:%.1f:%d:%s",
- elementId,
- x,
- y,
- width,
- height,
- radius,
- quality,
- tostring(isBackdrop)
- )
-end
-
---- Get cached blurred canvas
----@param key string Cache key
----@return love.Canvas|nil canvas Cached canvas or nil
-function Cache.getBlurredCanvas(key)
- local entry = Cache.blurredCanvases[key]
- if entry then
- entry.lastUsed = os.time()
- return entry.canvas
- end
- return nil
-end
-
---- Store blurred canvas in cache
----@param key string Cache key
----@param canvas love.Canvas Canvas to cache
-function Cache.setBlurredCanvas(key, canvas)
- -- Limit cache size
- local count = 0
- for _ in pairs(Cache.blurredCanvases) do
- count = count + 1
- end
-
- if count >= Cache.MAX_BLURRED_CANVAS_CACHE then
- -- Remove oldest entry
- local oldestKey = nil
- local oldestTime = math.huge
- for k, v in pairs(Cache.blurredCanvases) do
- if v.lastUsed < oldestTime then
- oldestTime = v.lastUsed
- oldestKey = k
- end
- end
-
- if oldestKey then
- if Cache.blurredCanvases[oldestKey].canvas then
- Cache.blurredCanvases[oldestKey].canvas:release()
- end
- Cache.blurredCanvases[oldestKey] = nil
- end
- end
-
- Cache.blurredCanvases[key] = {
- canvas = canvas,
- lastUsed = os.time(),
- }
-end
-
---- Clear blurred canvas cache for specific element
----@param elementId string Element ID to clear cache for
-function Cache.clearBlurredCanvasesForElement(elementId)
- for key, entry in pairs(Cache.blurredCanvases) do
- if key:match("^" .. elementId .. ":") then
- if entry.canvas then
- entry.canvas:release()
- end
- Cache.blurredCanvases[key] = nil
- end
- end
-end
-
---- Clear all caches
-function Cache.clear()
- -- Release all blurred canvases
- for _, entry in pairs(Cache.blurredCanvases) do
- if entry.canvas then
- entry.canvas:release()
- end
- end
-
- Cache.canvases = {}
- Cache.quads = {}
- Cache.blurInstances = {}
- Cache.blurredCanvases = {}
- warningCache = {} -- Clear warning cache on cache clear
-end
-
--- ============================================================================
--- SHADER BUILDER
--- ============================================================================
-
-local ShaderBuilder = {}
-
---- Build Gaussian blur shader with given parameters
----@param taps number Number of samples (must be odd, >= 3)
----@param offset number Offset value
----@param offsetType string "weighted" or "center"
----@param sigma number Sigma value for Gaussian distribution
----@return love.Shader shader The compiled blur shader
-function ShaderBuilder.build(taps, offset, offsetType, sigma)
- taps = math.floor(taps)
- sigma = sigma >= 1 and sigma or (taps - 1) * offset / 6
- sigma = math.max(sigma, 1)
-
- local steps = (taps + 1) / 2
-
- local gOffsets = {}
- local gWeights = {}
- for i = 1, steps do
- gOffsets[i] = offset * (i - 1)
- gWeights[i] = math.exp(-0.5 * (gOffsets[i] - 0) ^ 2 * 1 / sigma ^ 2)
- end
-
- local offsets = {}
- local weights = {}
- for i = #gWeights, 2, -2 do
- local oA, oB = gOffsets[i], gOffsets[i - 1]
- local wA, wB = gWeights[i], gWeights[i - 1]
- wB = oB == 0 and wB / 2 or wB
- local weight = wA + wB
- offsets[#offsets + 1] = offsetType == "center" and (oA + oB) / 2 or (oA * wA + oB * wB) / weight
- weights[#weights + 1] = weight
- end
-
- local code = {
- [[
- extern vec2 direction;
- vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {]],
- }
-
- local norm = 0
- if #gWeights % 2 == 0 then
- code[#code + 1] = "vec4 c = vec4( 0.0 );"
- else
- local weight = gWeights[1]
- norm = norm + weight
- code[#code + 1] = string.format("vec4 c = %f * texture2D(tex, tc);", weight)
- end
-
- local template = "c += %f * ( texture2D(tex, tc + %f * direction)+ texture2D(tex, tc - %f * direction));\n"
- for i = 1, #offsets do
- local offset = offsets[i]
- local weight = weights[i]
- norm = norm + weight * 2
- code[#code + 1] = string.format(template, weight, offset, offset)
- end
- code[#code + 1] = string.format("return c * vec4(%f) * color; }", 1 / norm)
-
- local shaderCode = table.concat(code)
- return love.graphics.newShader(shaderCode)
-end
-
---- Get or create a blur instance from cache
----@param quality number Quality level (1-10)
----@return table blurData Cached blur data {shader, taps}
-function Cache.getBlurInstance(quality)
- if not Cache.blurInstances[quality] then
- local taps = 3 + (quality - 1) * 1.5
- taps = math.floor(taps)
- if taps % 2 == 0 then
- taps = taps + 1
- end
-
- local shader = ShaderBuilder.build(taps, 1.0, "weighted", -1)
- Cache.blurInstances[quality] = {
- shader = shader,
- taps = taps,
- }
- end
-
- return Cache.blurInstances[quality]
-end
-
----@class BlurProps
----@field quality number? Quality level (1-10, default: 5)
-
----@class Blur
----@field shader love.Shader The blur shader
----@field quality number Quality level (1-10)
----@field taps number Number of shader taps
----@field _ErrorHandler table? Reference to ErrorHandler module
-local Blur = {}
-Blur.__index = Blur
-
---- Check if we should warn about large blur area in immediate mode
----@param elementId string|nil Element ID for caching warnings
----@param width number Blur area width
----@param height number Blur area height
----@param blurType string "content" or "backdrop"
-local function checkLargeBlurWarning(elementId, width, height, blurType)
- -- Skip if no ErrorHandler available
- if not Blur._ErrorHandler then
- return
- end
-
- -- Skip if not in immediate mode
- if not Blur._blurOptimizations then
- return
- end
-
- -- Calculate blur area
- local area = width * height
-
- -- Skip if area is below threshold
- if area <= Cache.LARGE_BLUR_THRESHOLD then
- return
- end
-
- -- Generate warning key (use elementId if available, otherwise use dimensions)
- local warningKey = elementId or string.format("%dx%d:%s", width, height, blurType)
-
- -- Skip if already warned for this element/area
- if warningCache[warningKey] then
- return
- end
-
- -- Mark as warned
- warningCache[warningKey] = true
-
- -- Issue warning
- local message =
- string.format("Large %s blur area detected (%dx%d = %d pixels) in immediate mode", blurType, width, height, area)
-
- local suggestion =
- "Consider using retained mode for this component to avoid recreating blur effects every frame. Large blur operations are expensive and can cause performance issues in immediate mode."
-
- Blur._ErrorHandler:warn("Blur", "PERF_003", {
- area = string.format("%.0fx%.0f", width or 0, height or 0),
- })
-end
-
---- Create a new blur effect instance
----@param props BlurProps? Blur configuration
----@return Blur blur The new blur instance
-function Blur.new(props)
- props = props or {}
-
- local quality = props.quality or 5
- quality = math.max(1, math.min(10, quality))
-
- -- Get cached blur instance for this quality level
- local blurData = Cache.getBlurInstance(quality)
-
- local self = setmetatable({}, Blur)
- self.shader = blurData.shader
- self.quality = quality
- self.taps = blurData.taps
-
- return self
-end
-
---- Apply blur to a region of the screen
----@param radius number Blur radius in pixels
----@param x number X position
----@param y number Y position
----@param width number Width of region
----@param height number Height of region
----@param drawFunc function Function to draw content to be blurred
-function Blur:applyToRegion(radius, x, y, width, height, drawFunc)
- if type(drawFunc) ~= "function" then
- if Blur._ErrorHandler then
- Blur._ErrorHandler:warn("Blur", "BLUR_001")
- end
- return
- end
-
- if radius <= 0 or width <= 0 or height <= 0 then
- drawFunc()
- return
- end
-
- -- Early exit for very low radius (optimization)
- if radius < Cache.RADIUS_THRESHOLD then
- drawFunc()
- return
- end
-
- -- Check for large blur area in immediate mode
- checkLargeBlurWarning(nil, width, height, "content")
-
- -- Calculate offset multiplier based on radius and quality
- -- Higher quality = more samples = smaller steps for same radius
- local offsetMultiplier = radius / self.quality
-
- local canvas1 = Cache.getCanvas(width, height)
- local canvas2 = Cache.getCanvas(width, height)
-
- local prevCanvas = love.graphics.getCanvas()
- local prevShader = love.graphics.getShader()
- local prevColor = { love.graphics.getColor() }
- local prevBlendMode = love.graphics.getBlendMode()
-
- love.graphics.setCanvas(canvas1)
- love.graphics.clear()
- love.graphics.push()
- love.graphics.origin()
- love.graphics.translate(-x, -y)
- drawFunc()
- love.graphics.pop()
-
- love.graphics.setShader(self.shader)
- love.graphics.setColor(1, 1, 1, 1)
- love.graphics.setBlendMode("alpha", "premultiplied")
-
- -- Single pass with radius-controlled offset
- love.graphics.setCanvas(canvas2)
- love.graphics.clear()
- self.shader:send("direction", { offsetMultiplier / width, 0 })
- love.graphics.draw(canvas1, 0, 0)
-
- love.graphics.setCanvas(canvas1)
- love.graphics.clear()
- self.shader:send("direction", { 0, offsetMultiplier / height })
- love.graphics.draw(canvas2, 0, 0)
-
- love.graphics.setCanvas(prevCanvas)
- love.graphics.setShader()
- love.graphics.setBlendMode(prevBlendMode)
- love.graphics.draw(canvas1, x, y)
-
- love.graphics.setShader(prevShader)
- love.graphics.setColor(unpack(prevColor))
-
- Cache.releaseCanvas(canvas1)
- Cache.releaseCanvas(canvas2)
-end
-
---- Apply backdrop blur effect (blur content behind a region)
----@param radius number Blur radius in pixels
----@param x number X position
----@param y number Y position
----@param width number Width of region
----@param height number Height of region
----@param backdropCanvas love.Canvas Canvas containing the backdrop content
-function Blur:applyBackdrop(radius, x, y, width, height, backdropCanvas)
- if not backdropCanvas then
- if Blur._ErrorHandler then
- Blur._ErrorHandler:warn("Blur", "BLUR_002")
- end
- return
- end
-
- if radius <= 0 or width <= 0 or height <= 0 then
- return
- end
-
- -- Early exit for very low radius (optimization)
- if radius < Cache.RADIUS_THRESHOLD then
- return
- end
-
- -- Calculate offset multiplier based on radius and quality
- local offsetMultiplier = radius / self.quality
-
- local canvas1 = Cache.getCanvas(width, height)
- local canvas2 = Cache.getCanvas(width, height)
-
- local prevCanvas = love.graphics.getCanvas()
- local prevShader = love.graphics.getShader()
- local prevColor = { love.graphics.getColor() }
- local prevBlendMode = love.graphics.getBlendMode()
-
- love.graphics.setCanvas(canvas1)
- love.graphics.clear()
- love.graphics.setColor(1, 1, 1, 1)
- love.graphics.setBlendMode("alpha", "premultiplied")
-
- local backdropWidth, backdropHeight = backdropCanvas:getDimensions()
- local quad = Cache.getQuad(x, y, width, height, backdropWidth, backdropHeight)
- love.graphics.draw(backdropCanvas, quad, 0, 0)
-
- love.graphics.setShader(self.shader)
-
- -- Single pass with radius-controlled offset
- love.graphics.setCanvas(canvas2)
- love.graphics.clear()
- self.shader:send("direction", { offsetMultiplier / width, 0 })
- love.graphics.draw(canvas1, 0, 0)
-
- love.graphics.setCanvas(canvas1)
- love.graphics.clear()
- self.shader:send("direction", { 0, offsetMultiplier / height })
- love.graphics.draw(canvas2, 0, 0)
-
- love.graphics.setCanvas(prevCanvas)
- love.graphics.setShader()
- love.graphics.setBlendMode(prevBlendMode)
- love.graphics.draw(canvas1, x, y)
-
- love.graphics.setShader(prevShader)
- love.graphics.setColor(unpack(prevColor))
-
- Cache.releaseCanvas(canvas1)
- Cache.releaseCanvas(canvas2)
- Cache.releaseQuad(quad)
-end
-
---- Get the current quality level
----@return number quality Quality level (1-10)
-function Blur:getQuality()
- return self.quality
-end
-
---- Get the number of shader taps
----@return number taps Number of shader taps
-function Blur:getTaps()
- return self.taps
-end
-
---- Clear all caches (call on window resize or memory cleanup)
-function Blur.clearCache()
- Cache.clear()
-end
-
---- Apply backdrop blur with caching support
----@param radius number Blur radius in pixels
----@param x number X position
----@param y number Y position
----@param width number Width of region
----@param height number Height of region
----@param backdropCanvas love.Canvas Canvas containing the backdrop content
----@param elementId string|nil Element ID for caching (nil disables caching)
-function Blur:applyBackdropCached(radius, x, y, width, height, backdropCanvas, elementId)
- -- If caching is disabled or no element ID, fall back to regular apply
- if not Blur._blurOptimizations or not elementId then
- return self:applyBackdrop(radius, x, y, width, height, backdropCanvas)
- end
-
- -- Generate cache key
- local cacheKey = Cache.generateBlurCacheKey(elementId, x, y, width, height, radius, self.quality, true)
-
- -- Check cache
- local cachedCanvas = Cache.getBlurredCanvas(cacheKey)
- if cachedCanvas then
- -- Draw cached blur
- local prevCanvas = love.graphics.getCanvas()
- local prevShader = love.graphics.getShader()
- local prevColor = { love.graphics.getColor() }
- local prevBlendMode = love.graphics.getBlendMode()
-
- love.graphics.setCanvas(prevCanvas)
- love.graphics.setShader()
- love.graphics.setBlendMode(prevBlendMode)
- love.graphics.draw(cachedCanvas, x, y)
-
- love.graphics.setShader(prevShader)
- love.graphics.setColor(unpack(prevColor))
- return
- end
-
- -- Not cached, render and cache
- if not backdropCanvas then
- if Blur._ErrorHandler then
- Blur._ErrorHandler:warn("Blur", "BLUR_002")
- end
- return
- end
-
- if radius <= 0 or width <= 0 or height <= 0 then
- return
- end
-
- -- Early exit for very low radius (optimization)
- if radius < Cache.RADIUS_THRESHOLD then
- return
- end
-
- -- Check for large blur area in immediate mode
- checkLargeBlurWarning(elementId, width, height, "backdrop")
-
- -- Calculate offset multiplier based on radius and quality
- local offsetMultiplier = radius / self.quality
-
- local canvas1 = Cache.getCanvas(width, height)
- local canvas2 = Cache.getCanvas(width, height)
-
- local prevCanvas = love.graphics.getCanvas()
- local prevShader = love.graphics.getShader()
- local prevColor = { love.graphics.getColor() }
- local prevBlendMode = love.graphics.getBlendMode()
-
- love.graphics.setCanvas(canvas1)
- love.graphics.clear()
- love.graphics.setColor(1, 1, 1, 1)
- love.graphics.setBlendMode("alpha", "premultiplied")
-
- local backdropWidth, backdropHeight = backdropCanvas:getDimensions()
- local quad = Cache.getQuad(x, y, width, height, backdropWidth, backdropHeight)
- love.graphics.draw(backdropCanvas, quad, 0, 0)
-
- love.graphics.setShader(self.shader)
-
- -- Single pass with radius-controlled offset
- love.graphics.setCanvas(canvas2)
- love.graphics.clear()
- self.shader:send("direction", { offsetMultiplier / width, 0 })
- love.graphics.draw(canvas1, 0, 0)
-
- love.graphics.setCanvas(canvas1)
- love.graphics.clear()
- self.shader:send("direction", { 0, offsetMultiplier / height })
- love.graphics.draw(canvas2, 0, 0)
-
- -- Cache the result
- local cachedResult = love.graphics.newCanvas(width, height)
- love.graphics.setCanvas(cachedResult)
- love.graphics.clear()
- love.graphics.setShader()
- love.graphics.setBlendMode("alpha", "premultiplied")
- love.graphics.draw(canvas1, 0, 0)
- Cache.setBlurredCanvas(cacheKey, cachedResult)
-
- love.graphics.setCanvas(prevCanvas)
- love.graphics.setShader()
- love.graphics.setBlendMode(prevBlendMode)
- love.graphics.draw(canvas1, x, y)
-
- love.graphics.setShader(prevShader)
- love.graphics.setColor(unpack(prevColor))
-
- Cache.releaseCanvas(canvas1)
- Cache.releaseCanvas(canvas2)
- Cache.releaseQuad(quad)
-end
-
---- Clear blur cache for specific element
----@param elementId string Element ID
-function Blur.clearElementCache(elementId)
- Cache.clearBlurredCanvasesForElement(elementId)
-end
-
---- Initialize Blur module with dependencies
----@param deps table Dependencies: { ErrorHandler = ErrorHandler?, immediateModeOptimizations = boolean? }
-function Blur.init(deps)
- if type(deps) == "table" then
- Blur._ErrorHandler = deps.ErrorHandler
- Blur._blurOptimizations = deps.immediateModeOptimizations or false
- end
-end
-
-Blur.Cache = Cache
-Blur.ShaderBuilder = ShaderBuilder
-
-return Blur
diff --git a/libs/flexlove/modules/Calc.lua b/libs/flexlove/modules/Calc.lua
deleted file mode 100644
index b62d6bb6..00000000
--- a/libs/flexlove/modules/Calc.lua
+++ /dev/null
@@ -1,385 +0,0 @@
---- Utility module for parsing and evaluating CSS-like calc() expressions
---- Supports arithmetic operations (+, -, *, /) with mixed units (px, %, vw, vh)
----@class Calc
-local Calc = {}
-
---- Initialize Calc module with dependencies
----@param deps CalcDependencies Dependencies: { ErrorHandler = ErrorHandler? }
-function Calc.init(deps)
- Calc._ErrorHandler = deps.ErrorHandler
-end
-
---- Token types for lexical analysis
-local TokenType = {
- NUMBER = "NUMBER",
- UNIT = "UNIT",
- PLUS = "PLUS",
- MINUS = "MINUS",
- MULTIPLY = "MULTIPLY",
- DIVIDE = "DIVIDE",
- LPAREN = "LPAREN",
- RPAREN = "RPAREN",
- EOF = "EOF",
-}
-
---- Tokenize a calc expression string into tokens
----@param expr string The expression to tokenize (e.g., "50% - 10vw")
----@return CalcToken[]? tokens Array of tokens with type, value, unit
----@return string? error Error message if tokenization fails
-local function tokenize(expr)
- local tokens = {}
- local i = 1
- local len = #expr
-
- while i <= len do
- local char = expr:sub(i, i)
-
- -- Skip whitespace
- if char:match("%s") then
- i = i + 1
- -- Number (including decimals, but NOT negative - handled separately below)
- elseif char:match("%d") or (char == "." and expr:sub(i + 1, i + 1):match("%d")) then
- local numStr = ""
-
- -- Parse integer and decimal parts
- while i <= len and (expr:sub(i, i):match("%d") or expr:sub(i, i) == ".") do
- numStr = numStr .. expr:sub(i, i)
- i = i + 1
- end
-
- local num = tonumber(numStr)
- if not num then
- return nil, "Invalid number: " .. numStr
- end
-
- -- Check for unit following the number
- local unitStr = ""
- while i <= len and expr:sub(i, i):match("[%a%%]") do
- unitStr = unitStr .. expr:sub(i, i)
- i = i + 1
- end
-
- -- Default to px if no unit
- if unitStr == "" then
- unitStr = "px"
- end
-
- -- Validate unit
- local validUnits = { px = true, ["%"] = true, vw = true, vh = true }
- if not validUnits[unitStr] then
- return nil, "Invalid unit: " .. unitStr
- end
-
- table.insert(tokens, {
- type = TokenType.NUMBER,
- value = num,
- unit = unitStr,
- })
- -- Operators
- elseif char == "+" then
- table.insert(tokens, { type = TokenType.PLUS })
- i = i + 1
- elseif char == "-" then
- -- Check if this is a negative number or subtraction
- -- It's a negative number if previous token is an operator or opening paren
- local prevToken = tokens[#tokens]
- if
- not prevToken
- or prevToken.type == TokenType.PLUS
- or prevToken.type == TokenType.MINUS
- or prevToken.type == TokenType.MULTIPLY
- or prevToken.type == TokenType.DIVIDE
- or prevToken.type == TokenType.LPAREN
- then
- -- This is a negative number, continue to number parsing
- local numStr = "-"
- i = i + 1
-
- -- Parse integer and decimal parts
- while i <= len and (expr:sub(i, i):match("%d") or expr:sub(i, i) == ".") do
- numStr = numStr .. expr:sub(i, i)
- i = i + 1
- end
-
- local num = tonumber(numStr)
- if not num then
- return nil, "Invalid number: " .. numStr
- end
-
- -- Check for unit following the number
- local unitStr = ""
- while i <= len and expr:sub(i, i):match("[%a%%]") do
- unitStr = unitStr .. expr:sub(i, i)
- i = i + 1
- end
-
- -- Default to px if no unit
- if unitStr == "" then
- unitStr = "px"
- end
-
- -- Validate unit
- local validUnits = { px = true, ["%"] = true, vw = true, vh = true }
- if not validUnits[unitStr] then
- return nil, "Invalid unit: " .. unitStr
- end
-
- table.insert(tokens, {
- type = TokenType.NUMBER,
- value = num,
- unit = unitStr,
- })
- else
- -- This is subtraction operator
- table.insert(tokens, { type = TokenType.MINUS })
- i = i + 1
- end
- elseif char == "*" then
- table.insert(tokens, { type = TokenType.MULTIPLY })
- i = i + 1
- elseif char == "/" then
- table.insert(tokens, { type = TokenType.DIVIDE })
- i = i + 1
- elseif char == "(" then
- table.insert(tokens, { type = TokenType.LPAREN })
- i = i + 1
- elseif char == ")" then
- table.insert(tokens, { type = TokenType.RPAREN })
- i = i + 1
- else
- return nil, "Unexpected character: " .. char
- end
- end
-
- table.insert(tokens, { type = TokenType.EOF })
- return tokens
-end
-
---- Parser for calc expressions using recursive descent
----@class Parser
----@field tokens CalcToken[] Array of tokens
----@field pos number Current token position
-local Parser = {}
-Parser.__index = Parser
-
---- Create a new parser
----@param tokens CalcToken[] Array of tokens
----@return Parser
-function Parser.new(tokens)
- local self = setmetatable({}, Parser)
- self.tokens = tokens
- self.pos = 1
- return self
-end
-
---- Get current token
----@return CalcToken token Current token
-function Parser:current()
- return self.tokens[self.pos]
-end
-
---- Advance to next token
-function Parser:advance()
- self.pos = self.pos + 1
-end
-
---- Parse expression (handles + and -)
----@return CalcASTNode ast Abstract syntax tree node
-function Parser:parseExpression()
- local left = self:parseTerm()
-
- while self:current().type == TokenType.PLUS or self:current().type == TokenType.MINUS do
- local op = self:current().type
- self:advance()
- local right = self:parseTerm()
- left = {
- type = op == TokenType.PLUS and "add" or "subtract",
- left = left,
- right = right,
- }
- end
-
- return left
-end
-
---- Parse term (handles * and /)
----@return CalcASTNode ast Abstract syntax tree node
-function Parser:parseTerm()
- local left = self:parseFactor()
-
- while self:current().type == TokenType.MULTIPLY or self:current().type == TokenType.DIVIDE do
- local op = self:current().type
- self:advance()
- local right = self:parseFactor()
- left = {
- type = op == TokenType.MULTIPLY and "multiply" or "divide",
- left = left,
- right = right,
- }
- end
-
- return left
-end
-
---- Parse factor (handles numbers and parentheses)
----@return CalcASTNode ast Abstract syntax tree node
-function Parser:parseFactor()
- local token = self:current()
-
- if token.type == TokenType.NUMBER then
- self:advance()
- return {
- type = "number",
- value = token.value,
- unit = token.unit,
- }
- elseif token.type == TokenType.LPAREN then
- self:advance()
- local expr = self:parseExpression()
- if self:current().type ~= TokenType.RPAREN then
- error("Expected closing parenthesis")
- end
- self:advance()
- return expr
- else
- error("Unexpected token: " .. token.type)
- end
-end
-
---- Parse the tokens into an AST
----@return CalcASTNode ast Abstract syntax tree
-function Parser:parse()
- local ast = self:parseExpression()
- if self:current().type ~= TokenType.EOF then
- error("Unexpected tokens after expression")
- end
- return ast
-end
-
---- Create a calc expression object that can be resolved later
---- This is the main API function that users call
----@param expr string The calc expression (e.g., "50% - 10vw")
----@return CalcObject calcObject A calc expression object with AST
-function Calc.new(expr)
- -- Tokenize
- local tokens, err = tokenize(expr)
- if not tokens then
- if Calc._ErrorHandler then
- Calc._ErrorHandler:warn("Calc", "VAL_006", {
- expression = expr,
- error = err,
- })
- end
- -- Return a fallback calc object that resolves to 0
- return {
- _isCalc = true,
- _expr = expr,
- _ast = nil,
- _error = err,
- }
- end
-
- -- Parse
- local parser = Parser.new(tokens)
- local success, ast = pcall(function()
- return parser:parse()
- end)
-
- if not success then
- if Calc._ErrorHandler then
- Calc._ErrorHandler:warn("Calc", "VAL_006", {
- expression = expr,
- error = ast, -- ast contains error message on failure
- })
- end
- -- Return a fallback calc object that resolves to 0
- return {
- _isCalc = true,
- _expr = expr,
- _ast = nil,
- _error = ast,
- }
- end
-
- return {
- _isCalc = true,
- _expr = expr,
- _ast = ast,
- }
-end
-
---- Check if a value is a calc expression
----@param value any The value to check
----@return boolean isCalc True if value is a calc expression
-function Calc.isCalc(value)
- return type(value) == "table" and value._isCalc == true
-end
-
---- Resolve a calc expression to pixel value
----@param calcObj CalcObject The calc expression object
----@param viewportWidth number Viewport width in pixels
----@param viewportHeight number Viewport height in pixels
----@param parentSize number? Parent dimension for percentage units
----@return number resolvedValue Resolved pixel value
-function Calc.resolve(calcObj, viewportWidth, viewportHeight, parentSize)
- if not calcObj._ast then
- -- Error during parsing, return 0
- return 0
- end
-
- --- Evaluate AST node recursively
- ---@param node table AST node
- ---@return number value Evaluated value in pixels
- local function evaluate(node)
- if node.type == "number" then
- -- Convert unit to pixels
- local value = node.value
- local unit = node.unit
-
- if unit == "px" then
- return value
- elseif unit == "%" then
- if not parentSize then
- if Calc._ErrorHandler then
- Calc._ErrorHandler:warn("Calc", "LAY_003", {
- unit = "%",
- issue = "parent dimension not available",
- })
- end
- return 0
- end
- return (value / 100) * parentSize
- elseif unit == "vw" then
- return (value / 100) * viewportWidth
- elseif unit == "vh" then
- return (value / 100) * viewportHeight
- else
- return 0
- end
- elseif node.type == "add" then
- return evaluate(node.left) + evaluate(node.right)
- elseif node.type == "subtract" then
- return evaluate(node.left) - evaluate(node.right)
- elseif node.type == "multiply" then
- return evaluate(node.left) * evaluate(node.right)
- elseif node.type == "divide" then
- local divisor = evaluate(node.right)
- if divisor == 0 then
- if Calc._ErrorHandler then
- Calc._ErrorHandler:warn("Calc", "VAL_006", {
- expression = calcObj._expr,
- error = "Division by zero",
- })
- end
- return 0
- end
- return evaluate(node.left) / divisor
- else
- return 0
- end
- end
-
- return evaluate(calcObj._ast)
-end
-
-return Calc
diff --git a/libs/flexlove/modules/Color.lua b/libs/flexlove/modules/Color.lua
deleted file mode 100644
index 1ea41d37..00000000
--- a/libs/flexlove/modules/Color.lua
+++ /dev/null
@@ -1,346 +0,0 @@
----@class Color
-local Color = {}
-Color.__index = Color
-
---- Initialize module with shared dependencies
----@param deps table Dependencies {ErrorHandler}
-function Color.init(deps)
- if type(deps) == "table" then
- Color._ErrorHandler = deps.ErrorHandler
- end
-end
-
---- Build type-safe color objects with automatic validation and clamping
---- Use this to avoid invalid color values and ensure consistent LÖVE-compatible colors (0-1 range)
----@param r number? Red component (0-1), defaults to 0
----@param g number? Green component (0-1), defaults to 0
----@param b number? Blue component (0-1), defaults to 0
----@param a number? Alpha component (0-1), defaults to 1
----@return Color color The new color instance
-function Color.new(r, g, b, a)
- -- Sanitize and clamp color components
- local _, sanitizedR = Color.validateColorChannel(r or 0, 1)
- local _, sanitizedG = Color.validateColorChannel(g or 0, 1)
- local _, sanitizedB = Color.validateColorChannel(b or 0, 1)
- local _, sanitizedA = Color.validateColorChannel(a or 1, 1)
-
- -- FFI structs don't support metatables/methods without wrapping
- -- The wrapping overhead negates the FFI benefits
- local self = setmetatable({}, Color)
- self.r = sanitizedR or 0
- self.g = sanitizedG or 0
- self.b = sanitizedB or 0
- self.a = sanitizedA or 1
- return self
-end
-
---- Extract individual color channels for use with love.graphics.setColor()
---- Use this to pass colors to LÖVE's rendering functions
----@return number r Red component (0-1)
----@return number g Green component (0-1)
----@return number b Blue component (0-1)
----@return number a Alpha component (0-1)
-function Color:toRGBA()
- return self.r, self.g, self.b, self.a
-end
-
---- Parse CSS-style hex colors into Color objects for designer-friendly workflows
---- Use this to work with colors from design tools that export hex values
----@param hexWithTag string Hex color string (e.g. "#RRGGBB" or "#RRGGBBAA")
----@return Color color The parsed color (returns white on error with warning)
-function Color.fromHex(hexWithTag)
- -- Validate input type
- if type(hexWithTag) ~= "string" then
- Color._ErrorHandler:warn("Color", "VAL_004", {
- input = tostring(hexWithTag),
- issue = "not a string",
- fallback = "white (#FFFFFF)",
- })
- return Color.new(1, 1, 1, 1)
- end
-
- local hex = hexWithTag:gsub("#", "")
- if #hex == 6 then
- local r = tonumber("0x" .. hex:sub(1, 2))
- local g = tonumber("0x" .. hex:sub(3, 4))
- local b = tonumber("0x" .. hex:sub(5, 6))
- if not r or not g or not b then
- Color._ErrorHandler:warn("Color", "VAL_004", {
- input = hexWithTag,
- issue = "invalid hex digits",
- fallback = "white (#FFFFFF)",
- })
- return Color.new(1, 1, 1, 1) -- Return white as fallback
- end
- return Color.new(r / 255, g / 255, b / 255, 1)
- elseif #hex == 8 then
- local r = tonumber("0x" .. hex:sub(1, 2))
- local g = tonumber("0x" .. hex:sub(3, 4))
- local b = tonumber("0x" .. hex:sub(5, 6))
- local a = tonumber("0x" .. hex:sub(7, 8))
- if not r or not g or not b or not a then
- Color._ErrorHandler:warn("Color", "VAL_004", {
- input = hexWithTag,
- issue = "invalid hex digits",
- fallback = "white (#FFFFFFFF)",
- })
- return Color.new(1, 1, 1, 1) -- Return white as fallback
- end
- return Color.new(r / 255, g / 255, b / 255, a / 255)
- else
- Color._ErrorHandler:warn("Color", "VAL_004", {
- input = hexWithTag,
- expected = "#RRGGBB or #RRGGBBAA",
- hexLength = #hex,
- fallback = "white (#FFFFFF)",
- })
- return Color.new(1, 1, 1, 1) -- Return white as fallback
- end
-end
-
---- Verify and sanitize individual color components to prevent rendering errors
---- Use this to safely process user input or external color data
----@param value any Value to validate
----@param max number? Maximum value (255 for 0-255 range, 1 for 0-1 range), defaults to 1
----@return boolean valid True if valid
----@return number? clamped Clamped value in 0-1 range, nil if invalid
-function Color.validateColorChannel(value, max)
- max = max or 1
-
- if type(value) ~= "number" then
- return false, nil
- end
-
- -- Check for NaN
- if value ~= value then
- return false, nil
- end
-
- -- Check for Infinity
- if value == math.huge or value == -math.huge then
- return false, nil
- end
-
- -- Normalize to 0-1 range
- local normalized = value
- if max == 255 then
- normalized = value / 255
- end
-
- -- Clamp to valid range
- normalized = math.max(0, math.min(1, normalized))
-
- return true, normalized
-end
-
---- Validate hex color format
----@param hex string Hex color string (with or without #)
----@return boolean valid True if valid format
----@return string? error Error message if invalid, nil if valid
-function Color.validateHexColor(hex)
- if type(hex) ~= "string" then
- return false, "Hex color must be a string"
- end
-
- -- Remove # prefix
- local cleanHex = hex:gsub("^#", "")
-
- -- Check length (3, 6, or 8 characters)
- if #cleanHex ~= 3 and #cleanHex ~= 6 and #cleanHex ~= 8 then
- return false, string.format("Invalid hex length: %d. Expected 3, 6, or 8 characters", #cleanHex)
- end
-
- -- Check for valid hex characters
- if not cleanHex:match("^[0-9A-Fa-f]+$") then
- return false, "Invalid hex characters. Use only 0-9, A-F"
- end
-
- return true, nil
-end
-
---- Validate RGB/RGBA color values
----@param r number Red component
----@param g number Green component
----@param b number Blue component
----@param a number? Alpha component (optional, defaults to max)
----@param max number? Maximum value (255 or 1), defaults to 1
----@return boolean valid True if valid
----@return string? error Error message if invalid, nil if valid
-function Color.validateRGBColor(r, g, b, a, max)
- max = max or 1
- a = a or max
-
- local rValid = Color.validateColorChannel(r, max)
- local gValid = Color.validateColorChannel(g, max)
- local bValid = Color.validateColorChannel(b, max)
- local aValid = Color.validateColorChannel(a, max)
-
- if not rValid then
- return false, string.format("Invalid red channel: %s", tostring(r))
- end
- if not gValid then
- return false, string.format("Invalid green channel: %s", tostring(g))
- end
- if not bValid then
- return false, string.format("Invalid blue channel: %s", tostring(b))
- end
- if not aValid then
- return false, string.format("Invalid alpha channel: %s", tostring(a))
- end
-
- return true, nil
-end
-
---- Check if a value is a valid color format
----@param value any Value to check
----@return string? format Format type ("hex", "named", "table"), nil if invalid
-function Color.isValidColorFormat(value)
- local valueType = type(value)
-
- -- Check for hex string
- if valueType == "string" then
- if value:match("^#?[0-9A-Fa-f]+$") then
- local valid = Color.validateHexColor(value)
- if valid then
- return "hex"
- end
- end
-
- return nil
- end
-
- -- Check for table format
- if valueType == "table" then
- -- Check for Color instance
- if getmetatable(value) == Color then
- return "table"
- end
-
- -- Check for array format {r, g, b, a}
- if value[1] and value[2] and value[3] then
- local valid = Color.validateRGBColor(value[1], value[2], value[3], value[4])
- if valid then
- return "table"
- end
- end
-
- -- Check for named format {r=, g=, b=, a=}
- if value.r and value.g and value.b then
- local valid = Color.validateRGBColor(value.r, value.g, value.b, value.a)
- if valid then
- return "table"
- end
- end
-
- return nil
- end
-
- return nil
-end
-
---- Convert any color format to a valid Color object with graceful fallbacks
---- Use this to robustly handle colors from any source without crashes
----@param value any Color value to sanitize (hex, named, table, or Color instance)
----@param default Color? Default color if invalid (defaults to black)
----@return Color color Sanitized color instance (guaranteed non-nil)
-function Color.sanitizeColor(value, default)
- default = default or Color.new(0, 0, 0, 1)
-
- local format = Color.isValidColorFormat(value)
-
- if not format then
- return default
- end
-
- -- Handle hex format
- if format == "hex" then
- local cleanHex = value:gsub("^#", "")
-
- -- Expand 3-digit hex to 6-digit
- if #cleanHex == 3 then
- cleanHex = cleanHex:gsub("(.)", "%1%1")
- end
-
- -- Try to parse
- local success, result = pcall(Color.fromHex, "#" .. cleanHex)
- if success then
- return result
- else
- return default
- end
- end
-
- if format == "table" then
- -- Color instance
- if getmetatable(value) == Color then
- return value
- end
-
- -- Array format
- if value[1] then
- local _, r = Color.validateColorChannel(value[1], 1)
- local _, g = Color.validateColorChannel(value[2], 1)
- local _, b = Color.validateColorChannel(value[3], 1)
- local _, a = Color.validateColorChannel(value[4] or 1, 1)
-
- if r and g and b and a then
- return Color.new(r, g, b, a)
- end
- end
-
- -- Named format
- if value.r then
- local _, r = Color.validateColorChannel(value.r, 1)
- local _, g = Color.validateColorChannel(value.g, 1)
- local _, b = Color.validateColorChannel(value.b, 1)
- local _, a = Color.validateColorChannel(value.a or 1, 1)
-
- if r and g and b and a then
- return Color.new(r, g, b, a)
- end
- end
- end
-
- return default
-end
-
---- Universally convert any color format (hex, named, table) into a Color object
---- Use this as your main color input handler to accept flexible color specifications
----@param value any Color value (hex string, named color, table, or Color instance)
----@return Color color Parsed color instance (defaults to black on error)
-function Color.parse(value)
- return Color.sanitizeColor(value, Color.new(0, 0, 0, 1))
-end
-
---- Smoothly transition between two colors for animations and gradients
---- Use this to create color-based animations without manual channel calculations
----@param colorA Color Starting color
----@param colorB Color Ending color
----@param t number Interpolation factor (0-1)
----@return Color color Interpolated color
-function Color.lerp(colorA, colorB, t)
- -- Sanitize inputs
- if type(colorA) ~= "table" or getmetatable(colorA) ~= Color then
- colorA = Color.new(0, 0, 0, 1)
- end
- if type(colorB) ~= "table" or getmetatable(colorB) ~= Color then
- colorB = Color.new(0, 0, 0, 1)
- end
- if type(t) ~= "number" or t ~= t or t == math.huge or t == -math.huge then
- t = 0
- end
-
- -- Clamp t to 0-1 range
- t = math.max(0, math.min(1, t))
-
- -- Linear interpolation for each channel
- local oneMinusT = 1 - t
- local r = colorA.r * oneMinusT + colorB.r * t
- local g = colorA.g * oneMinusT + colorB.g * t
- local b = colorA.b * oneMinusT + colorB.b * t
- local a = colorA.a * oneMinusT + colorB.a * t
-
- return Color.new(r, g, b, a)
-end
-
-return Color
diff --git a/libs/flexlove/modules/Context.lua b/libs/flexlove/modules/Context.lua
deleted file mode 100644
index 10f53015..00000000
--- a/libs/flexlove/modules/Context.lua
+++ /dev/null
@@ -1,596 +0,0 @@
----@class Context
-local modulePath = (...):match("(.-)[^%.]+$")
-local ZIndex = require(modulePath .. "ZIndex")
-local Element = require(modulePath .. "Element")
-local Context = {
- topElements = {},
- -- Base scale configuration
- baseScale = nil, -- {width: number, height: number}
- -- Current scale factors
- scaleFactors = { x = 1.0, y = 1.0 },
- defaultTheme = nil,
- _focusedElement = nil,
- _focusedElementId = nil, -- Stable id used to rehydrate focus across immediate-mode frames
- _activeEventElement = nil,
- _cachedViewport = { width = 0, height = 0 },
- -- Immediate mode state
- _immediateMode = false,
- _frameNumber = 0,
- _currentFrameElements = {},
- _immediateModeState = nil, -- Will be initialized if immediate mode is enabled
- _frameStarted = false,
- _autoBeganFrame = false,
- -- Z-index ordered element tracking for immediate mode
- _zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest)
- -- Focus management guard
- _settingFocus = false,
- -- Hook called whenever focus changes: function(element) or nil
- _onFocusChanged = nil,
-
- -- Navigation state
- _navigationContext = {
- lastFocusedElement = nil, -- For returning from modals
- navigationMode = "sequential", -- "sequential" or "directional"
- containerElement = nil, -- Current navigation container
- },
-
- initialized = false,
-
- -- Expose internal hit-testing helpers for unit testing only.
- -- These are populated below after their local definitions. They are NOT part
- -- of the public API and must not be relied on by callers; they exist so the
- -- shared hit-test core (the single place display:none guarding lives) can be
- -- exercised directly by the test suite. Subsequent unified-event-routing
- -- tasks consume these locals through the mode-agnostic query functions.
- _test = {
- pointHitsElement = nil,
- elementHasScrollableOverflow = nil,
- },
-
- -- Debug draw overlay
- _debugDraw = false,
- _debugDrawKey = nil,
-
- -- Initialization state tracking
- ---@type "uninitialized"|"initializing"|"ready"
- _initState = "uninitialized",
- ---@type table[] Queue of {props: ElementProps, callback: function(element)|nil}
- _initQueue = {},
-
- -- Per-frame cache for findInteractiveAtPosition so Clickable.onUpdate's
- -- per-element call (unified-event-routing task 05) doesn't re-walk the tree
- -- + realloc + sort for every interactive element sharing the same cursor.
- -- Invalidated explicitly by Context.clearInteractiveCache() at the start of
- -- each flexlove.update (both modes) and in clearFrameElements (immediate
- -- mid-frame rebuild). It also self-invalidates when the topElements table
- -- reference changes (tests replace it per-case; immediate-mode beginFrame
- -- reassigns it each frame), so direct callers that never go through
- -- flexlove.update still see fresh results across tree swaps.
- _interactiveLookupCache = {
- valid = false,
- x = nil,
- y = nil,
- result = nil,
- topElementsRef = nil,
- frameNumber = -1,
- },
-}
-
---- Check if a point hits an element, accounting for scroll offsets and display:none.
---- All mode-agnostic query functions use this as their single hit-test entry point,
---- ensuring fixes like display:none guarding apply everywhere.
----
---- This is the single canonical place where `element.display == false` short-
---- circuits hit testing. Parent-chain clipping/scroll-offset accumulation is
---- the caller's responsibility: callers walk the parent chain (using
---- `elementHasScrollableOverflow` to decide which ancestors clip) and pass the
---- accumulated scroll offset in here. Keeping the parent walk outside this core
---- lets retained-mode (recursive tree descent) and immediate-mode (flat
---- z-index list) callers share the exact same primitive bounds/display logic.
----@param element Element
----@param mx number Screen X coordinate
----@param my number Screen Y coordinate
----@param scrollOffsetX number? Accumulated scroll offset from parent chain
----@param scrollOffsetY number? Accumulated scroll offset from parent chain
----@return boolean hits
-local function pointHitsElement(element, mx, my, scrollOffsetX, scrollOffsetY)
- scrollOffsetX = scrollOffsetX or 0
- scrollOffsetY = scrollOffsetY or 0
-
- -- Skip display:none elements entirely
- if element.display == false then
- return false
- end
-
- local bx = element.x
- local by = element.y
- local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
-
- local adjustedX = mx + scrollOffsetX
- local adjustedY = my + scrollOffsetY
-
- return adjustedX >= bx and adjustedX <= bx + bw and adjustedY >= by and adjustedY <= by + bh
-end
-
---- Check if an element has scrollable/clipped overflow (for scroll offset accumulation).
---- Returns true for `scroll`, `auto`, and `hidden` on either axis. These are the
---- overflow values that clip/translate descendant content and therefore require
---- scroll-offset compensation when hit testing descendants.
----@param element Element
----@return boolean
-local function elementHasScrollableOverflow(element)
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- return overflowX == "scroll"
- or overflowX == "auto"
- or overflowY == "scroll"
- or overflowY == "auto"
- or overflowX == "hidden"
- or overflowY == "hidden"
-end
-
--- Expose the two core helpers for unit testing only (see Context._test above).
-Context._test.pointHitsElement = pointHitsElement
-Context._test.elementHasScrollableOverflow = elementHasScrollableOverflow
-
--- Public exposure of the canonical hit-test primitive so other modules
--- (e.g. FlexLove's `getElementAtPosition` / `_getTouchElementAtPosition`
--- tree walks) can share the single implementation of bounds + display:none
--- guarding instead of duplicating the `display == false` check inline.
--- This keeps "display == false" in exactly one place for hit-testing.
-Context.pointHitsElement = pointHitsElement
-Context.elementHasScrollableOverflow = elementHasScrollableOverflow
-
---- Find the first scrollable element at a screen position, regardless of mode.
---- This is the mode-agnostic successor to the two duplicated scrollable lookups
---- that previously lived inline in `flexlove.wheelmoved`:
---- * immediate mode — walked `Context._zIndexOrderedElements` in reverse and
---- re-implemented bounds + parent-chain clipping + scroll-offset math; and
---- * retained mode — recursed through `Context.topElements` with a private
---- `findScrollableAtPosition(elements, x, y)` helper.
---- Both paths now collapse into this single function, which routes every
---- hit test through `pointHitsElement` (the single place `display == false`
---- is guarded) and every scroll-offset decision through
---- `elementHasScrollableOverflow`. As a result display:none elements are never
---- returned in either mode, fixing the latent bug where the immediate-mode
---- path's `isPointInElement` did not skip display:none elements.
----
---- The retained-mode branch intentionally mirrors the original
---- `findScrollableAtPosition` helper's tree walk (deepest scrollable wins,
---- children checked before self) but is upgraded to thread accumulated scroll
---- offsets through `pointHitsElement` so nested scrolled containers are tested
---- against their visible position. The original helper is removed once
---- `flexlove.wheelmoved` is rerouted onto this function in task 04.
----@param x number Screen X coordinate
----@param y number Screen Y coordinate
----@return Element|nil The scrollable element, or nil
-function Context.findScrollableAtPosition(x, y)
- if Context.isImmediateMode() then
- -- Immediate mode: iterate the z-index ordered list (reverse order =
- -- topmost first). pointHitsElement supplies the bounds + display guard.
- for i = #Context._zIndexOrderedElements, 1, -1 do
- local element = Context._zIndexOrderedElements[i]
- if pointHitsElement(element, x, y) then
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- if
- (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto")
- and (element._overflowX or element._overflowY)
- then
- return element
- end
- end
- end
- return nil
- else
- -- Retained mode: recursive tree walk from topElements. Children are
- -- checked before self (deepest scrollable wins); accumulated scroll
- -- offsets are threaded through pointHitsElement so descendants of
- -- scrolled containers are hit-tested against their translated position.
- local function findInTree(elements, scrollOffsetX, scrollOffsetY)
- scrollOffsetX = scrollOffsetX or 0
- scrollOffsetY = scrollOffsetY or 0
- for i = #elements, 1, -1 do
- local element = elements[i]
- if pointHitsElement(element, x, y, scrollOffsetX, scrollOffsetY) then
- if #element.children > 0 then
- local childScrollOffsetX = scrollOffsetX
- local childScrollOffsetY = scrollOffsetY
- if elementHasScrollableOverflow(element) then
- childScrollOffsetX = childScrollOffsetX + (element._scrollX or 0)
- childScrollOffsetY = childScrollOffsetY + (element._scrollY or 0)
- end
- local childResult = findInTree(element.children, childScrollOffsetX, childScrollOffsetY)
- if childResult then
- return childResult
- end
- end
- -- No descendant was scrollable — check self.
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- if
- (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto")
- and (element._overflowX or element._overflowY)
- then
- return element
- end
- end
- end
- return nil
- end
- return findInTree(Context.topElements)
- end
-end
-
---- Check whether immediate mode is active.
---- This is the single canonical accessor for the mode flag consumed throughout
---- the framework. Mode-aware branches elsewhere call this instead of reading
---- `Context._immediateMode` directly, so the literal mode flag only appears
---- here (its definition) and in StateManager (its mirrored storage) — never
---- scattered across Element / behaviors / managers (behavior-mode-unification
---- task 11).
----@return boolean
-function Context.isImmediateMode()
- return Context._immediateMode
-end
-
----@return number, number -- scaleX, scaleY
-function Context.getScaleFactors()
- return Context.scaleFactors.x, Context.scaleFactors.y
-end
-
---- Register an element in the z-index ordered tree (for immediate mode)
----@param element Element The element to register
-function Context.registerElement(element)
- if not Context.isImmediateMode() then
- return
- end
-
- table.insert(Context._zIndexOrderedElements, element)
-end
-
-function Context.clearFrameElements()
- Context._zIndexOrderedElements = {}
- Context.clearInteractiveCache()
-end
-
---- Compute the composite z-index key for an element.
---- rootZ * ROOT_WEIGHT + depth * DEPTH_WEIGHT + ownZ
----
---- ROOT_WEIGHT (10^10) gives the top-level ancestor's z-index 10 digits of significance.
---- DEPTH_WEIGHT (10^3) gives nesting depth 3 digits, ensuring children always sort above
---- their ancestors. The element's own z (capped to ±999 by ZIndex.clamp) fits within the
---- remaining 3 digits without interfering with the depth component.
----
---- These weights assume |z| <= ZIndex.MAX_Z and practical tree depths (< 10^7), which
---- keeps the composite key well within Lua's exact integer range (2^53 ≈ 9 × 10^15).
----
---- This is the SINGLE canonical z-index ordering function, used by both
---- sortElementsByZIndex (the immediate-mode flat list sort) and
---- findInteractiveAtPosition (the mode-agnostic occlusion sort). Keeping them
---- on the same key ensures the interactive topmost element matches the visual
---- draw order — a button in a z=50 MainMenu window must occlude a button in a
---- z=0 BottomBar even when both buttons default to own z=0.
-local function getEffectiveZIndex(elem)
- local ownZ = elem.z or 0
- local rootZ = ownZ
- local depth = 0
- local current = elem.parent
- while current do
- rootZ = current.z or 0
- depth = depth + 1
- current = current.parent
- end
- return rootZ * ZIndex.ROOT_WEIGHT + depth * ZIndex.DEPTH_WEIGHT + ownZ
-end
-
--- Public exposure so FlexLove.getElementAtPosition shares the single
--- implementation instead of duplicating the parent-chain walk as a closure.
-Context.getEffectiveZIndex = getEffectiveZIndex
-
---- Sort elements by z-index (called after all elements are registered)
-function Context.sortElementsByZIndex()
- -- Precompute the composite key ONCE per element so the sort comparator is a
- -- pure table lookup (O(1)) instead of re-walking the parent chain on every
- -- O(N log N) comparison. This function runs every frame in immediate mode.
- local elements = Context._zIndexOrderedElements
- local zIndices = {}
- for i = 1, #elements do
- zIndices[elements[i]] = getEffectiveZIndex(elements[i])
- end
- table.sort(elements, function(a, b)
- return zIndices[a] < zIndices[b]
- end)
-end
-
---- Find the topmost interactive element at a screen position, regardless of mode.
---- Replaces the former immediate-mode-only `Context.getTopElementAt()` (removed
---- in unified-event-routing task 05) and the retained-mode `_activeEventElement`
---- mechanism — both are now funneled through this single entry point.
----
---- In immediate mode this replaces Context.getTopElementAt() (which only worked
---- in immediate mode). In retained mode this provides the same role as the
---- _activeEventElement set by flexlove.getElementAtPosition().
----
---- An element is "interactive" if it has an onEvent handler, themeComponent, or is editable.
----@param x number Screen X coordinate
----@param y number Screen Y coordinate
----@return Element|nil The topmost interactive element, or nil
-function Context.findInteractiveAtPosition(x, y)
- -- Per-frame cache: Clickable.onUpdate runs this for every interactive
- -- element under the same cursor, but the result for a given (x,y) is
- -- identical across all of them within a single update pass. Returning a
- -- cached element restores the old 1x/frame cost of the _activeEventElement
- -- mechanism that task 05 replaced. Cache auto-invalidates when the
- -- topElements table reference changes (so tests and mid-frame rebuilds get
- -- fresh results) and is cleared explicitly per-frame in flexlove.update.
- local cache = Context._interactiveLookupCache
- if
- cache.valid
- and cache.x == x
- and cache.y == y
- and cache.topElementsRef == Context.topElements
- and cache.frameNumber == Context._frameNumber
- then
- return cache.result
- end
-
- local interactiveCandidates = {}
-
- local function collectInteractive(element, scrollOffsetX, scrollOffsetY)
- scrollOffsetX = scrollOffsetX or 0
- scrollOffsetY = scrollOffsetY or 0
-
- if not pointHitsElement(element, x, y, scrollOffsetX, scrollOffsetY) then
- return
- end
-
- -- Check if this element is interactive
- if element.onEvent or element.themeComponent or element.editable then
- table.insert(interactiveCandidates, element)
- end
-
- -- Recurse into children with accumulated scroll offset
- local childScrollOffsetX = scrollOffsetX
- local childScrollOffsetY = scrollOffsetY
- if elementHasScrollableOverflow(element) then
- childScrollOffsetX = childScrollOffsetX + (element._scrollX or 0)
- childScrollOffsetY = childScrollOffsetY + (element._scrollY or 0)
- end
-
- for _, child in ipairs(element.children) do
- collectInteractive(child, childScrollOffsetX, childScrollOffsetY)
- end
- end
-
- -- Always traverse the tree (works in both modes — topElements exists always)
- for _, element in ipairs(Context.topElements) do
- collectInteractive(element)
- end
-
- -- Sort by composite z-index descending — topmost wins. The composite key
- -- (rootZ * ROOT_WEIGHT + depth * DEPTH_WEIGHT + ownZ) matches the ordering
- -- used by sortElementsByZIndex / _zIndexOrderedElements, so the interactive
- -- topmost element matches the visual draw order. This is critical for the
- -- game's multi-window layout: a button inside a z=50 MainMenu window must
- -- occlude a button inside a z=0 BottomBar even when both buttons default to
- -- own z=0. Sorting by own-z alone (the original implementation) couldn't
- -- distinguish them, so the wrong window's button could win, leaving the
- -- visible button's isActiveElement=false and clicks/hover dead.
- local zIndices = {}
- for _, el in ipairs(interactiveCandidates) do
- zIndices[el] = getEffectiveZIndex(el)
- end
- table.sort(interactiveCandidates, function(a, b)
- return zIndices[a] > zIndices[b]
- end)
-
- local result = interactiveCandidates[1]
-
- cache.x = x
- cache.y = y
- cache.result = result
- cache.topElementsRef = Context.topElements
- cache.frameNumber = Context._frameNumber
- cache.valid = true
-
- return result
-end
-
---- Invalidate the per-frame `findInteractiveAtPosition` cache.
---- Called once at the top of `flexlove.update` (the natural per-frame boundary
---- in both modes) and from `clearFrameElements` (immediate-mode mid-frame
---- rebuild). After invalidation the next lookup recomputes fresh.
-function Context.clearInteractiveCache()
- local cache = Context._interactiveLookupCache
- cache.valid = false
- cache.x = nil
- cache.y = nil
- cache.result = nil
- cache.topElementsRef = nil
- cache.frameNumber = -1
-end
-
---- Set the focused element (centralizes focus management)
---- Automatically blurs the previously focused element if different
----@param element Element|nil The element to focus (nil to clear focus)
-function Context.setFocused(element)
- if Context._focusedElement == element then
- return -- Already focused
- end
-
- -- Prevent re-entry during focus change
- if Context._settingFocus then
- return
- end
- Context._settingFocus = true
-
- -- Save reference to previously focused element before updating
- local oldFocusedElement = Context._focusedElement
-
- -- Blur previously focused element
- if oldFocusedElement and oldFocusedElement ~= element then
- if oldFocusedElement._textEditor then
- oldFocusedElement._textEditor:blur(oldFocusedElement)
- end
- end
-
- -- Set new focused element and persist its id for immediate-mode rehydration
- Context._focusedElement = element
- Context._focusedElementId = element and (element.id ~= "" and element.id or nil) or nil
-
- -- Notify any registered focus change hook (e.g. FocusIndicator)
- if Context._onFocusChanged then
- Context._onFocusChanged(element)
- end
-
- -- Focus the new element's text editor if it has one
- if element and element._textEditor then
- element._textEditor._focused = true
- end
-
- Context._settingFocus = false
-end
-
---- Recursively search for an element by id in an element tree
----@param root Element The root element to start searching from
----@param targetId string The id to search for
----@return Element|nil The element with the matching id, or nil if not found
-local function findElementById(root, targetId)
- if root.id == targetId then
- return root
- end
- for _, child in ipairs(root.children or {}) do
- local found = findElementById(child, targetId)
- if found then
- return found
- end
- end
- return nil
-end
-
---- Rehydrate _focusedElement from _focusedElementId by scanning live elements.
---- Called at the start of getFocused() in immediate mode so stale references
---- are always replaced with the current-frame object before use.
-function Context._rehydrateFocus()
- if not Context._focusedElementId then
- Context._focusedElement = nil
- return
- end
-
- -- First, try a fast linear search through all registered elements
- for _, elem in ipairs(Context._zIndexOrderedElements) do
- if elem.id == Context._focusedElementId then
- Context._focusedElement = elem
- return
- end
- end
-
- -- If not found, recursively search from top-level elements
- -- This handles cases where elements may not be in _zIndexOrderedElements
- for _, topLevel in ipairs(Context.topElements or {}) do
- local found = findElementById(topLevel, Context._focusedElementId)
- if found then
- Context._focusedElement = found
- return
- end
- end
-
- -- Element with that id is not present this frame (e.g. screen changed)
- Context._focusedElement = nil
-end
-
---- Get the currently focused element
----@return Element|nil The focused element, or nil if none
-function Context.getFocused()
- if Context.isImmediateMode() then
- Context._rehydrateFocus()
- end
- return Context._focusedElement
-end
-
---- Clear focus from any element
-function Context.clearFocus()
- Context._focusedElementId = nil
- Context.setFocused(nil)
-end
-
---- Get all focusable elements in tab order, regardless of mode.
---- In immediate mode this extracts from _zIndexOrderedElements (flat, z-sorted).
---- In retained mode it walks the element tree (DOM order).
---- In both modes, display:none elements are excluded.
----@return table List of focusable elements in tab order
-function Context.getFocusableElements()
- local focusable = {}
-
- local function isFocusable(elem)
- if elem.display == false then
- return false
- end
- -- Use Element:isFocusable() for consistent behavior
- return Element.isFocusable(elem)
- end
-
- local function collectFromTree(elements)
- for _, elem in ipairs(elements) do
- if isFocusable(elem) then
- table.insert(focusable, elem)
- end
- if #elem.children > 0 then
- collectFromTree(elem.children)
- end
- end
- end
-
- if Context._immediateMode then
- -- Immediate mode: _zIndexOrderedElements is already in z-index order (lowest first),
- -- which approximates tab order for most UIs.
- for _, elem in ipairs(Context._zIndexOrderedElements) do
- if isFocusable(elem) then
- table.insert(focusable, elem)
- end
- end
- else
- -- Retained mode: walk the top element trees in DOM order
- collectFromTree(Context.topElements)
- end
-
- return focusable
-end
-
--- ====================
--- Navigation Context
--- ====================
-
---- Push current focus onto stack (for modals/dialogs)
----@param element Element?
-function Context.pushFocusStack(element)
- Context._navigationContext.lastFocusedElement = Context._focusedElement
- if element then
- Context.setFocused(element)
- end
-end
-
---- Pop focus from stack (return from modal)
----@return Element?
-function Context.popFocusStack()
- local previous = Context._navigationContext.lastFocusedElement
- Context._navigationContext.lastFocusedElement = nil
- Context.setFocused(previous)
- return previous
-end
-
---- Set navigation container (scope for tab navigation)
----@param element Element?
-function Context.setNavigationContainer(element)
- Context._navigationContext.containerElement = element
-end
-
---- Get navigation container
----@return Element?
-function Context.getNavigationContainer()
- return Context._navigationContext.containerElement
-end
-
-return Context
diff --git a/libs/flexlove/modules/Element.lua b/libs/flexlove/modules/Element.lua
deleted file mode 100644
index 5a2da8db..00000000
--- a/libs/flexlove/modules/Element.lua
+++ /dev/null
@@ -1,3904 +0,0 @@
----@class Element
----@field id string
----@field children Element[]
----@field parent Element|nil
----@field userdata any|nil
----@field onEvent fun(self: Element, event: table)|nil
----@field onEventDeferred boolean|nil
----@field onFocus fun(self: Element)|nil
----@field onFocusDeferred boolean
----@field dropFocusOnSelection boolean|nil
----@field onBlur fun(self: Element)|nil
----@field onBlurDeferred boolean
----@field onTextInput fun(self: Element, text: string)|nil
----@field onTextInputDeferred boolean
----@field onTextChange fun(self: Element, text: string)|nil
----@field onTextChangeDeferred boolean
----@field onEnter fun(self: Element)|nil
----@field onEnterDeferred boolean
----@field customDraw fun(self: Element)|nil
----@field onTouchEvent fun(self: Element, event: table)|nil
----@field onTouchEventDeferred boolean
----@field onGesture fun(self: Element, gesture: table)|nil
----@field onGestureDeferred boolean
----@field touchEnabled boolean
----@field multiTouchEnabled boolean
----@field theme table|nil
----@field themeComponent string|nil
----@field disabled boolean
----@field active boolean
----@field disableHighlight boolean
----@field contentAutoSizingMultiplier number[]|nil
----@field scaleCorners boolean|nil
----@field scalingAlgorithm string|nil
----@field contentBlur {radius:number, quality?:number}|nil
----@field backdropBlur {radius:number, quality?:number}|nil
----@field editable boolean
----@field multiline boolean
----@field passwordMode boolean
----@field textWrap string|boolean
----@field maxLines number|nil
----@field maxLength number|nil
----@field placeholder string|nil
----@field inputType string
----@field textOverflow string
----@field scrollable boolean
----@field autoGrow boolean
----@field selectOnFocus boolean
----@field cursorColor Color|nil
----@field selectionColor Color|nil
----@field cursorBlinkRate number
----@field selectParent Element|nil
----@field selectOption table|nil
----@field onChange fun(self: Element, value: any, option: Element)|nil
----@field border number|table|nil
----@field borderColor Color
----@field backgroundColor Color
----@field opacity number
----@field visibility string
----@field display boolean
----@field transform table|nil
----@field cornerRadius number|{topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}|nil
----@field text string|nil
----@field textAlign string|table|nil
----@field textAlignHorizontal string
----@field textAlignVertical string
----@field imagePath string|nil
----@field image table|nil
----@field objectFit string
----@field objectPosition string
----@field imageOpacity number
----@field imageRepeat string
----@field imageTint Color|nil
----@field onImageLoad fun(self: Element, image: table)|nil
----@field onImageLoadDeferred boolean
----@field onImageError fun(self: Element, err: string)|nil
----@field onImageErrorDeferred boolean
----@field prevGameSize {width:number, height:number}
----@field autosizing {width:boolean, height:boolean}
----@field units table
----@field minTextSize number|nil
----@field maxTextSize number|nil
----@field autoScaleText boolean
----@field fontFamily string|nil
----@field textSize number
----@field width number
----@field height number
----@field x number
----@field y number
----@field z number
----@field gap number
----@field flexGrow number
----@field flexShrink number
----@field flexBasis number|string
----@field padding {top:number, right:number, bottom:number, left:number}
----@field margin {top:number, right:number, bottom:number, left:number}
----@field tabIndex number|nil
----@field textColor Color
----@field positioning string
----@field top number|nil
----@field right number|nil
----@field bottom number|nil
----@field left number|nil
----@field flexDirection string|nil
----@field flexWrap string|nil
----@field justifyContent string|nil
----@field alignItems string|nil
----@field alignContent string|nil
----@field justifySelf string|nil
----@field alignSelf string
----@field gridRows number|nil
----@field gridColumns number|nil
----@field columnGap number|nil
----@field rowGap number|nil
----@field transition table
----@field transitions table|nil
----@field animation table|nil
----@field overflow string|nil
----@field overflowX string|nil
----@field overflowY string|nil
----@field scrollbarWidth number|nil
----@field scrollbarColor Color|nil
----@field scrollbarBackgroundColor Color|nil
----@field scrollbarTrackColor Color|nil
----@field scrollbarRadius number|nil
----@field scrollbarPadding number|nil
----@field scrollSpeed number|nil
----@field invertScroll boolean|nil
----@field scrollBarStyle string|nil
----@field scrollbarKnobOffset number|nil
----@field hideScrollbars boolean|nil
----@field scrollbarPlacement string|nil
----@field scrollbarBalance number|nil
----@field borderWidth number|nil
----@field fontSize number|nil
----@field lineHeight number|nil
-local Element = {}
-Element.__index = Element
-
--- Forward declarations for the special-handler binding helpers used by
--- Element:_applyProps (behavior-mode-unification task 08 capstone). These
--- absorb the former subsystem-init and visual-state phase bodies (ThemeManager
--- creation + theme-field exposure + editable/text/scroll/autoGrow/select-field
--- defaults + parent assignment, and border/cornerRadius/display/text/textAlign
--- normalization). They are now private implementation of the props-binding phase
--- rather than standalone Element methods, so there are no longer per-capability
--- init phases on Element.
-local bindThemeAndFields, bindVisualState
-
--- NOTE: There is intentionally NO custom Element.__newindex for dimension properties.
--- Lua's __newindex fires ONLY when the key is ABSENT from the raw table, but width/
--- height/x/y are all assigned during Element.new, so they already exist post-
--- construction. A __newindex handler therefore CANNOT intercept retained-mode bare
--- writes like `element.width = "42%"` (it just rawsets the broken string).
--- Dimensions are instead validated lazily in Element:_checkDimensionTypes() at the
--- start of each reflow, and must be changed via :setProperty() for resolution +
--- layout invalidation. Keeping the metatable free of __newindex also avoids a
--- per-field-write function call on every absent-key assignment (perf).
-
-local MAX_DEFER_RETRIES = 10
-local MAX_DEFERRED_METHODS = 100
-local _DEFERRED_NIL = {}
-local unpack = table.unpack or unpack
-
----Initialize Element module with required dependencies
----@param deps table Dependency table containing all required modules
-function Element.init(deps)
- Element._ErrorHandler = deps.ErrorHandler
- Element._Color = deps.Color
- Element._Context = deps.Context
- Element._Units = deps.Units
- Element._Calc = deps.Calc
- Element._utils = deps.utils
- Element._InputEvent = deps.InputEvent
- Element._EventHandler = deps.EventHandler
- Element._Renderer = deps.Renderer
- Element._LayoutEngine = deps.LayoutEngine
- Element._TextEditor = deps.TextEditor
- Element._ScrollManager = deps.ScrollManager
- Element._Theme = deps.Theme
- Element._RoundedRect = deps.RoundedRect
- Element._NinePatch = deps.NinePatch
- Element._ImageRenderer = deps.ImageRenderer
- Element._ImageCache = deps.ImageCache
- Element._ImageScaler = deps.ImageScaler
- Element._Blur = deps.Blur
- Element._Transform = deps.Transform
- Element._Grid = deps.Grid
- Element._StateManager = deps.StateManager
- Element._GestureRecognizer = deps.GestureRecognizer
- Element._Performance = deps.Performance
- Element._Animation = deps.Animation
- Element._ZIndex = deps.ZIndex
- Element._Select = deps.Select
- Element._PropertySchema = deps.PropertySchema or require("modules.PropertySchema")
- Element._Select.init({
- ErrorHandler = Element._ErrorHandler,
- Context = Element._Context,
- StateManager = Element._StateManager,
- utils = Element._utils,
- Element = Element,
- })
- Element._ScrollManager.init({
- ErrorHandler = Element._ErrorHandler,
- Context = Element._Context,
- StateManager = Element._StateManager,
- })
-
- -- Bind Element scroll/scrollbar API directly onto ScrollManager.
- -- ScrollManager owns all scroll interaction logic; Element retains only
- -- 1-line delegates (no hand-written sync/nil-guard boilerplate).
- local SM = Element._ScrollManager
- Element._syncScrollManagerState = SM.syncToElement
- Element._detectOverflow = SM._detectOverflow
- Element.setScrollPosition = SM.setScrollPosition
- Element._calculateScrollbarDimensions = SM._calculateScrollbarDimensions
- Element._getScrollbarAtPosition = SM._getScrollbarAtPosition
- Element._handleScrollbarPress = SM._handleScrollbarPress
- Element._handleScrollbarDrag = SM._handleScrollbarDrag
- Element._handleScrollbarRelease = SM._handleScrollbarRelease
- Element._handleWheelScroll = SM._handleWheelScroll
- Element.getScrollPosition = SM.getScrollPosition
- Element.getMaxScroll = SM.elementGetMaxScroll
- Element.getScrollPercentage = SM.elementGetScrollPercentage
- Element.hasOverflow = SM.elementHasOverflow
- Element.getContentSize = SM.elementGetContentSize
- Element.scrollBy = SM.elementScrollBy
- Element.scrollToTop = SM.scrollToTop
- Element.scrollToBottom = SM.scrollToBottom
- Element.scrollToLeft = SM.scrollToLeft
- Element.scrollToRight = SM.scrollToRight
-
- -- Hoist subsystem dependency tables: created once at init time, not rebuilt
- -- per Element.new() call. Staged initializers reference these directly.
- Element._eventHandlerDeps = {
- InputEvent = Element._InputEvent,
- Context = Element._Context,
- utils = Element._utils,
- }
-
- -- Behavior registry (behavior-mode-unification). Concrete behaviors live in
- -- modules/behaviors/ and auto-attach during Element.new when their
- -- shouldAttach(props) predicate returns true. Element.update/draw/save-restore
- -- dispatch over `element.behaviors` instead of branching on capability flags.
- -- Registry order matters for onDraw layering: Themed (core Renderer:draw) must
- -- run before Clickable (pressed overlay) so pressed feedback paints on top.
- -- Imageable (image config) runs last. Animated (task 06) is late-attach-only.
- -- Task 02 wires Clickable; task 05 Selectable; task 06 Animated; task 07
- -- Themed + Imageable; task 04 TextEditable (cursor blink + TextEditor
- -- ownership + the 27 text-delegate forwarders). Scrollable is pending.
- Element._behaviorRegistry = deps.behaviors or deps.clickableBehaviors or {}
- -- TextEditable module reference: Element's 1-line text-delegate forwarders
- -- route through `Element._TextEditable.(self, ...)` (task 04). Resolved
- -- from deps (wired by FlexLove alongside the behavior registry) so minimal
- -- builds without TextEditable leave forwarders inert (guarded by their
- -- callers / the behavior's nil-checks).
- Element._TextEditable = deps.TextEditable
- -- Cached lookup of the Animated behavior instance for late-attach. Resolved
- -- lazily (behaviors are optional in minimal builds) the first time an
- -- animation is created on an element.
- Element._animatedBehavior = nil
- Element._rendererDeps = {
- Color = Element._Color,
- RoundedRect = Element._RoundedRect,
- NinePatch = Element._NinePatch,
- ImageRenderer = Element._ImageRenderer,
- ImageCache = Element._ImageCache,
- Theme = Element._Theme,
- Blur = Element._Blur,
- Transform = Element._Transform,
- utils = Element._utils,
- }
- Element._layoutEngineDeps = {
- utils = Element._utils,
- Grid = Element._Grid,
- Units = Element._Units,
- Context = Element._Context,
- ErrorHandler = Element._ErrorHandler,
- }
- Element._textEditorDeps = {
- Context = Element._Context,
- StateManager = Element._StateManager,
- Color = Element._Color,
- utils = Element._utils,
- }
- Element._scrollManagerDeps = {
- utils = Element._utils,
- Color = Element._Color,
- }
-end
-
--- Module-level helper: resolve a dimensional property with CSS-like unit support (px, %, vw, vh, calc)
--- Defined once (not inside new()) to avoid per-element closure allocation.
--- Handles parsing, defensive checks, and storage in both self and self.units tables.
----@param self table Element instance
----@param raw any Raw property value (string, number, CalcObject, or nil)
----@param key string Field name on self and self.units (e.g., "width", "x")
----@param ref number Reference dimension for percentage resolution
----@param ctx {vw:number, vh:number, sx:number, sy:number} Viewport and scale context
----@param opts {offset?: number, scaleAxis?: "x"|"y", default?: number, nullable?: boolean}?
----@return number? resolved Resolved pixel value (or nil if opts.nullable and input is missing/invalid)
-local function _resolveUnit(self, raw, key, ref, ctx, opts)
- opts = opts or {}
- if raw == nil then
- if opts.nullable then
- return nil
- end
- local default = opts.default or 0
- self[key] = (opts.offset or 0) + default
- self.units[key] = { value = default, unit = "px" }
- return self[key]
- end
- local isCalc = Element._Calc and Element._Calc.isCalc(raw)
- if type(raw) == "string" or isCalc then
- local value, unit = Element._Units.parse(raw)
- local resolved = Element._Units.resolve(value, unit, ctx.vw, ctx.vh, ref)
- if type(resolved) ~= "number" then
- if opts.nullable then
- return nil
- end
- Element._ErrorHandler:warn("Element", "LAY_003", {
- issue = key .. " resolution returned non-number value",
- type = type(resolved),
- value = tostring(resolved),
- })
- resolved = 0
- end
- self.units[key] = { value = value, unit = unit }
- self[key] = (opts.offset or 0) + resolved
- else
- local val = raw
- if opts.scaleAxis and Element._Context.baseScale then
- val = raw * (opts.scaleAxis == "x" and ctx.sx or ctx.sy)
- end
- self[key] = (opts.offset or 0) + val
- self.units[key] = { value = raw, unit = "px" }
- end
- return self[key]
-end
-
--- Module-level helper: re-resolve a stored unit spec against a new viewport/parent reference.
--- Used by resize() to refresh min/max constraints declared with %/vw/vh units.
-local function _refreshUnit(self, key, ref, ctx, scaleAxis)
- local u = self.units[key]
- if not u or u.value == nil then
- return
- end
- if u.unit == "px" then
- self[key] = Element._Context.baseScale and (u.value * (scaleAxis == "x" and ctx.sx or ctx.sy)) or u.value
- return
- end
- local resolved = Element._Units.resolve(u.value, u.unit, ctx.vw, ctx.vh, ref)
- self[key] = type(resolved) == "number" and resolved or nil
-end
-
--- ---------------------------------------------------------------------------
--- Consolidated warn helpers (Task 11).
--- Each duplicated "expecting X, got Y" / guard instrumentation block lived at
--- its own use site; these single-reference helpers centralize the emit so call
--- sites are thin invocations. Validation semantics (warn+fallback vs. throw) are
--- preserved exactly — instrumentation is consolidated, not deleted.
--- ---------------------------------------------------------------------------
-
--- Emit a VAL_001 invalid-enum warn for a textAlign sub-field and return the
--- fallback. textAlign's schema entry is type "any" (string | table | compound),
--- so this IS the boundary validator for the 4 textAlign parse branches in the
--- props-phase visual-state helper.
-local function _warnTextAlign(field, expected, got, fallback)
- Element._ErrorHandler:warn("Element", "VAL_001", {
- property = field,
- expected = expected,
- got = tostring(got),
- })
- return fallback
-end
-
--- Emit a FLEX_00x warn for an invalid flexGrow/flexShrink/flexBasis and return
--- the fallback value. These props are SPECIAL_PROPS (warn+fallback, not throw)
--- so this is their boundary validator.
-local function _warnFlexInvalid(self, code, issue, value, fallback)
- Element._ErrorHandler:warn("Element", code, {
- element = self.id or "unnamed",
- issue = issue,
- value = tostring(value),
- })
- return fallback
-end
-
--- Emit an ELEM_010/011/012 warn for malformed declarative children entries.
-local function _warnChildrenInvalid(self, code, issue, value)
- local details = { element = self.id or "unnamed", issue = issue }
- if value ~= nil then
- details.value = tostring(value)
- end
- Element._ErrorHandler:warn("Element", code, details)
-end
-
--- Emit LAY_011 when CSS positioning props (top/right/bottom/left) are supplied
--- without absolute positioning. Called from both the no-parent and with-parent
--- branches of _initPositioning.
-local function _warnCssPositioningWithoutAbsolute(self, props)
- local properties = {}
- if props.top then
- table.insert(properties, "top")
- end
- if props.bottom then
- table.insert(properties, "bottom")
- end
- if props.left then
- table.insert(properties, "left")
- end
- if props.right then
- table.insert(properties, "right")
- end
- Element._ErrorHandler:warn("Element", "LAY_011", {
- element = self.id or "unnamed",
- positioning = self._originalPositioning or "relative",
- properties = table.concat(properties, ", "),
- })
-end
-
--- Emit an ELEM_003/004/005 guard warn for the animation/transition public API
--- (deps-missing, non-table arg, invalid duration, non-table property list).
--- All warn + fall back rather than throw. `value` nil => warn carries no details.
-local function _warnAnimApi(code, value)
- if value ~= nil then
- Element._ErrorHandler:warn("Element", code, { value = tostring(value) })
- else
- Element._ErrorHandler:warn("Element", code)
- end
-end
-
--- Image loading + image callback firing now live in the Imageable behavior
--- (modules/behaviors/Imageable.lua) — moved out of Element per
--- behavior-mode-unification task 07. Element is decoupled from image concern;
--- the Imageable behavior enriches `element._renderer` with image config, runs
--- the deferred load pipeline, and persists `_loadedImage` across immediate-mode
--- frames. The fire-callback helper (formerly `_fireImageCallback` here) is
--- reproduced inside Imageable as `fireImageCallback`.
-
--- ---------------------------------------------------------------------------
--- Data-driven prop binding (Task 03)
--- ---------------------------------------------------------------------------
--- SPECIAL_PROPS is the documented boundary of the schema-driven _applyProps
--- loop. Props listed here are bound explicitly by bindThemeAndFields /
--- bindVisualState / the staged initializers instead of the generic registry
--- loop, for one of five load-bearing reasons (none represent unfinished
--- migration — moving them into the generic loop would require extending the
--- PropertySchema DSL, which is deliberately kept small and declarative):
---
--- 1. SUBSYSTEM ORDERING — the prop needs a subsystem constructed first.
--- Theme props (theme/themeComponent/disabled/active/...) depend on the
--- ThemeManager being alive so their defaults can be read from it; the
--- generic loop runs before subsystem creation in _attachBehaviors.
---
--- 2. NON-LITERAL DEFAULTS — the default is not a static value the schema's
--- `default:` field can express. borderColor/backgroundColor/textColor
--- default to Color.new(...); text defaults to "" only when editable;
--- scrollable/autoGrow default from multiline. The schema only stores
--- literal defaults (pure-Lua constraint; see PropertySchema.lua header).
---
--- 3. UNIT RESOLUTION / VIEWPORT CONTEXT — dimension props (width/height/x/y
--- /gap/padding/...) accept unit strings ("50%", "10px") or CalcObjects that
--- resolve against parent size and viewport, which a pure-Lua schema cannot
--- see. setProperty routes these via the `isDimension` flag at runtime, but
--- construction-time binding needs the sizing context from _initSizingContext.
---
--- 4. WARN-AND-FALLBACK vs. THROW — display and the flex props validate with a
--- non-throwing warn+fallback path; the schema's `validator` field throws
--- (VAL_001) for invalid enum/range values. These need their own boundary
--- validators (_warnFlexInvalid / the display type-check).
---
--- 5. SUBSYSTEM OWNERSHIP — overflow/scrollbar* are owned by ScrollManager,
--- selectParent/selectOption by the Select subsystem, border/cornerRadius
--- use schema normalizers but bind with a special shape (all-false→nil).
--- These props' storage is owned by their subsystem, not the element core.
---
--- Adding a new SIMPLE prop requires only a PropertySchema entry; adding a prop
--- that needs any of the above additionally requires listing it here and binding
--- it in the matching special-handler phase. Props NOT listed here (e.g.
--- callbacks, editable, multiline, passwordMode, autoScaleText, cursorColor,
--- selectionColor, opacity, visibility, transform, imagePath/objectFit/...,
--- minTextSize/maxTextSize, alignSelf, transition) are bound generically by
--- _applyProps (defaults + normalizers + validators + onX/onXDeferred
--- auto-wiring).
-local function _set(...)
- local t = {}
- for _, name in ipairs({ ... }) do
- t[name] = true
- end
- return t
-end
-
-local SPECIAL_PROPS = _set(
- -- identity / tree
- "id",
- "parent",
- "children",
- -- theme-driven (ThemeManager owns these / computes defaults)
- "theme",
- "themeComponent",
- "disabled",
- "isDisabled",
- "active",
- "disableHighlight",
- "themeStateLock",
- "themeComponentDisabledStates",
- "scaleCorners",
- "scalingAlgorithm",
- "contentAutoSizingMultiplier",
- -- color defaults that require the Color module
- "borderColor",
- "backgroundColor",
- "textColor",
- -- display: non-throwing warn+fallback (unlike throwing range/enum validators)
- "display",
- -- text editing (validation side-effects / computed defaults)
- "textWrap",
- "scrollable",
- "autoGrow",
- "text",
- "textAlign",
- "textAlignHorizontal",
- "textAlignVertical",
- "textSize",
- "fontFamily",
- -- box model / dimensions (unit resolution)
- "width",
- "height",
- "x",
- "y",
- "z",
- "minWidth",
- "maxWidth",
- "minHeight",
- "maxHeight",
- "gap",
- "top",
- "right",
- "bottom",
- "left",
- "columnGap",
- "rowGap",
- "padding",
- "margin",
- -- layout enums (positioning-mode validation + LayoutEngine config)
- "positioning",
- "flexDirection",
- "flexWrap",
- "justifyContent",
- "alignItems",
- "alignContent",
- "justifySelf",
- "gridRows",
- "gridColumns",
- -- flex shorthand + validated numerics (custom FLEX_xx warnings)
- "flex",
- "flexGrow",
- "flexShrink",
- "flexBasis",
- -- scroll / scrollbar (ScrollManager owns these)
- "overflow",
- "overflowX",
- "overflowY",
- "scrollbarWidth",
- "scrollbarColor",
- "scrollbarTrackColor",
- "scrollbarRadius",
- "scrollbarPadding",
- "scrollSpeed",
- "invertScroll",
- "smoothScrollEnabled",
- "scrollBarStyle",
- "scrollbarKnobOffset",
- "hideScrollbars",
- "scrollbarPlacement",
- "scrollbarBalance",
- "_scrollX",
- "_scrollY",
- -- select (Select subsystem owns these)
- "selectParent",
- "selectOption",
- -- border / cornerRadius use schema normalizers but are bound as special handlers
- "border",
- "cornerRadius",
- -- misc instance-only fields derived during construction
- "tabIndex"
-)
-
---- Bind every schema-driven, side-effect-free prop onto `self` in one pass.
---- Iterates PropertySchema entries: applies defaults, normalizers, validators,
---- and auto-wires `onX` + `onXDeferred` companion pairs. Props listed in
---- SPECIAL_PROPS are skipped (they are handled explicitly in Element.new).
----@param props table Element construction props
-function Element:_applyProps(props)
- local schema = Element._PropertySchema
- local registry = schema.all()
- for name, meta in pairs(registry) do
- -- Skip deferred companion entries (auto-wired by their base callback's
- -- hasDeferred branch below) and SPECIAL_PROPS (handled in Element.new).
- if not (name:match("Deferred$") or SPECIAL_PROPS[name]) then
- local value = props[name]
- if value == nil then
- value = meta.default
- end
- if meta.normalizer then
- value = meta.normalizer(value)
- end
- if meta.validator and value ~= nil and not meta.validator(value) then
- -- Mirror the legacy throwing validateRange/validateEnum behavior: invalid
- -- enum/range values error during construction (validated props: opacity,
- -- imageOpacity, objectFit, imageRepeat). display is a special handler that
- -- warns + falls back instead.
- Element._ErrorHandler:error("Element", "VAL_001", {
- property = name,
- expected = meta.type,
- got = tostring(value),
- })
- end
- local key = meta.storageKey or name
- self[key] = value
- -- Auto-wire deferred companion for callbacks that declare hasDeferred.
- if meta.hasDeferred then
- local deferredName = name .. "Deferred"
- local deferredValue = props[deferredName]
- self[deferredName] = deferredValue ~= nil and deferredValue or false
- end
- end
- end
-
- -- Special-handler binding (behavior-mode-unification task 08 capstone): the
- -- props below need side-effects, ordering relative to subsystems, non-literal
- -- defaults, or unit resolution, so they cannot be bound by the generic schema
- -- loop above. The two helpers below fold in the former subsystem-init phase
- -- (ThemeManager creation + theme-field exposure + editable/multiline/
- -- passwordMode validation + textWrap/scrollable/autoGrow defaults +
- -- selectParent/selectOption/_selectState + parent assignment) and visual-state
- -- phase (border/cornerRadius/display/text/textAlign normalization). Subsystem
- -- CREATION (EventHandler / TextEditor / ScrollManager / Renderer) is owned by
- -- behavior onAttach hooks dispatched in _attachBehaviors; these helpers only
- -- bind FIELDS that the core sizing/box/positioning phases and the behavior
- -- onAttach hooks read.
- bindThemeAndFields(self, props)
- bindVisualState(self, props)
-end
-
----@param props ElementProps
----@return Element
---- Construct a new Element. Orchestrator only; real work is in the staged
---- initializers below (Task 10). No single phase exceeds ~400 LOC.
-function Element.new(props)
- -- Staged initializers (behavior-mode-unification task 08 capstone). The
- -- orchestrator is a thin dispatcher: it runs core-data phases only
- -- (construct → props → sizing → box model → positioning → finalize), then
- -- attaches behaviors. The former behavioral phases (subsystem-init, visual-
- -- state, image/renderer, scroll-manager) are deleted: their field-binding logic
- -- folded into _applyProps and their subsystem creation logic moved into
- -- behavior onAttach hooks (Clickable / TextEditable / Selectable / Themed /
- -- Imageable / Scrollable). _attachBehaviors runs at the tail so
- -- Selectable.onAttach can re-scan declarative children built by
- -- _finalizeConstruction and so onAttach sees all element fields bound.
- local self = Element:_construct(props)
- self:_applyProps(props)
- self:_initSizingContext(props)
- self:_initBoxModel(props)
- self:_initPositioning(props)
- self:_finalizeConstruction(props)
- self:_attachBehaviors(props)
- return self
-end
-
---- Phase 1: metatable, schema-driven prop normalization (for special-handler
---- props), default tables (children, _deferredMethods), and ID generation.
---- Schema-driven binding (_applyProps) is invoked separately by Element.new.
-function Element:_construct(props)
- local instance = setmetatable({}, Element)
-
- -- Stash the construction props so behavior onAttach hooks (which receive only
- -- the element per the locked `(element, ...)` signature) can read SPECIAL_PROPS
- -- config that is NOT bound onto the element by the schema-driven _applyProps
- -- loop (e.g. the scrollbar config consumed by Scrollable.onAttach). Prefixed
- -- with `_` so the immediate-mode saveState public-prop scan skips it.
- instance._initProps = props
-
- -- Apply schema-driven shape normalizers to props for the special-handler
- -- props (padding/margin/flexDirection) whose downstream unit-resolution logic
- -- reads from `props` directly. Generic props are bound by _applyProps below.
- local schema = Element._PropertySchema
- props.flexDirection = schema.get("flexDirection").normalizer(props.flexDirection)
- props.padding = schema.get("padding").normalizer(props.padding)
- props.margin = schema.get("margin").normalizer(props.margin)
-
- -- Behavior registry (Task 01 of behavior-mode-unification). Concrete
- -- behaviors (Clickable, Scrollable, ...) are attached here in later tasks;
- -- Element:update/draw/save-restore dispatch over this table instead of
- -- branching on individual capability flags. Initially empty so existing
- -- behavior is identical to pre-refactor until behaviors are wired in.
- instance.children = {}
- instance.behaviors = {}
- instance._deferredMethods = {}
-
- -- Track whether ID was auto-generated (before ID assignment)
- local idWasAutoGenerated = not props.id or props.id == ""
-
- -- Auto-generate ID if not provided (for all elements)
- if idWasAutoGenerated then
- instance.id = Element._StateManager.generateID(props, props.parent)
- else
- instance.id = props.id
- end
-
- -- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
- instance._stateId = instance.id
-
- -- Register with StateManager for state access (both immediate and retained modes)
- if instance._stateId and instance._stateId ~= "" then
- Element._StateManager.registerStateful(instance._stateId, instance)
- end
- return instance
-end
-
---- Attach behaviors whose shouldAttach(props) predicate matches this element's
---- props. Runs after prop binding + subsystem init (so Deferred flags and the
---- Select subsystem are in place) and dispatches onAttach for each match. The
---- EventHandler (Clickable) is created here rather than in the former subsystem-
---- init phase so Element never needs to know what an individual behavior does —
---- it only iterates the registry (behavior-mode-unification task 02).
-function Element:_attachBehaviors(props)
- local registry = Element._behaviorRegistry
- if registry then
- for _, behavior in ipairs(registry) do
- if behavior.shouldAttach(props) then
- table.insert(self.behaviors, behavior)
- behavior.onAttach(self)
- end
- end
- end
-end
-
---- Resolve the (lazily cached) Animated behavior instance from the registry.
--- task 09: since the behavior loop no longer excludes Animated, elements WITH
--- the behavior attached get the loop dispatch and `_dispatchAnimatedUpdate` no-ops
--- for them.
--- task 09 consolidation: `_ensureAnimatedAttached` / `_isAnimatedBehavior`
--- (dead code) were removed; Animated.ensureAttached remains the late-attach
--- entry point for callers that route through it.
-function Element._resolveAnimatedBehavior()
- local animated = Element._animatedBehavior
- if animated == nil then
- local registry = Element._behaviorRegistry
- if registry then
- for _, behavior in ipairs(registry) do
- -- Animated exposes ensureAttached; Clickable/Themed/Imageable do not.
- if type(behavior.ensureAttached) == "function" then
- animated = behavior
- break
- end
- end
- end
- Element._animatedBehavior = animated or false
- end
- return animated
-end
-
---- Dispatch Animated.onUpdate for an element EARLY in Element:update (before
---- the behavior loop) so animated geometry (x/y/width/height) is current for
---- Clickable hit-testing and Scrollable interaction this frame. NO-OPS for
---- elements that already have the Animated behavior attached (the loop
---- dispatches those) to avoid a double update — animation:update(dt) is not
---- idempotent within a frame. This handles the direct-assignment path
---- (`element.animation = ...` / `anim:apply`) that bypasses
---- Animated.ensureAttached; for elements with no animation the behavior's
---- onUpdate reads `element.animation` and returns. Not a behavioral capability
---- branch — iterates `element.behaviors`. (behavior-mode-unification task 09.)
-function Element._dispatchAnimatedUpdate(element, dt)
- if not element then
- return
- end
- local animated = Element._resolveAnimatedBehavior()
- if not animated then
- return
- end
- -- Already attached? The behavior loop will dispatch it; bail to avoid a
- -- double update (animation:update advances twice if called twice).
- local behaviors = element.behaviors
- if behaviors then
- for i = 1, #behaviors do
- if behaviors[i] == animated then
- return
- end
- end
- end
- animated.onUpdate(element, dt)
-end
-
---- Special-handler binding helper for the props phase (behavior-mode-
---- unification task 08). Formerly the Element subsystem-init phase. Binds the
---- ThemeManager (or no-op fallback) + exposes theme fields, validates
---- editable/multiline/passwordMode combos, sets textWrap/scrollable/autoGrow
---- defaults, initializes selectParent/selectOption/_selectState fields (the
---- Select subsystem itself is initialized by Selectable.onAttach), and assigns
---- self.parent. EventHandler creation is owned by Clickable.onAttach and
---- TextEditor creation by TextEditable.onAttach — both run in _attachBehaviors at
---- the tail of Element.new, so this helper does not touch either subsystem.
-bindThemeAndFields = function(self, props)
- if Element._Theme then
- self._themeManager = Element._Theme.Manager.new({
- theme = props.theme or Element._Context.defaultTheme,
- themeComponent = props.themeComponent or nil,
- disabled = props.isDisabled or props.disabled or false,
- active = props.active or false,
- disableHighlight = props.disableHighlight,
- themeStateLock = props.themeStateLock or false,
- themeComponentDisabledStates = props.themeComponentDisabledStates,
- scaleCorners = props.scaleCorners,
- scalingAlgorithm = props.scalingAlgorithm,
- })
- else
- -- Theme module absent (minimal build) — plain no-op ThemeManager
- local noPadding = { top = 0, right = 0, bottom = 0, left = 0 }
- self._themeManager = {
- theme = nil,
- themeComponent = props.themeComponent or nil,
- disabled = props.isDisabled or props.disabled or false,
- active = props.active or false,
- themeComponentDisabledStates = {},
- scaleCorners = props.scaleCorners,
- scalingAlgorithm = props.scalingAlgorithm,
- validateThemeStateLock = function() end,
- getState = function()
- return "normal"
- end,
- setState = function() end,
- updateState = function()
- return false
- end,
- hasThemeComponent = function()
- return false
- end,
- getTheme = function()
- return nil
- end,
- getComponent = function()
- return nil
- end,
- getStateComponent = function()
- return nil
- end,
- getScrollbarComponent = function()
- return nil
- end,
- getDefaultFontFamily = function()
- return nil
- end,
- getContentAutoSizingMultiplier = function()
- return nil
- end,
- getScaledContentPadding = function()
- return noPadding
- end,
- getScaledContentPaddingForState = function()
- return noPadding
- end,
- _getScaledContentPaddingForState = function()
- return noPadding
- end,
- getStyle = function()
- return nil
- end,
- }
- end
-
- -- Validate themeStateLock after ThemeManager is created
- if props.themeStateLock and props.themeComponent then
- self._themeManager:validateThemeStateLock()
- end
-
- -- Expose theme properties for backward compatibility
- self.theme = self._themeManager.theme
- self.themeComponent = self._themeManager.themeComponent
- self.disabled = self._themeManager.disabled
- self.active = self._themeManager.active
- self._themeState = self._themeManager:getState()
-
- -- disableHighlight defaults to true when using themeComponent (themes handle their own visual feedback)
- -- Can be explicitly overridden by setting props.disableHighlight
- if props.disableHighlight ~= nil then
- self.disableHighlight = props.disableHighlight
- else
- self.disableHighlight = self.themeComponent ~= nil
- end
-
- -- Initialize contentAutoSizingMultiplier after theme is set
- -- Priority: element props > theme component > theme default
- if props.contentAutoSizingMultiplier then
- self.contentAutoSizingMultiplier = props.contentAutoSizingMultiplier
- else
- local multiplier = self._themeManager:getContentAutoSizingMultiplier()
- self.contentAutoSizingMultiplier = multiplier or { 1, 1 }
- end
-
- -- Expose 9-patch corner scaling properties for backward compatibility
- self.scaleCorners = self._themeManager.scaleCorners
- self.scalingAlgorithm = self._themeManager.scalingAlgorithm
-
- self._blurInstance = nil
-
- -- editable/multiline/passwordMode are bound by _applyProps (default false).
- -- Validate combinations: passwordMode disables multiline.
- if self.passwordMode and self.multiline then
- Element._ErrorHandler:warn("Element", "ELEM_006")
- self.multiline = false
- elseif self.passwordMode then
- self.multiline = false
- end
-
- self.textWrap = props.textWrap
- if self.textWrap == nil then
- self.textWrap = self.multiline and "word" or false
- end
-
- self.scrollable = props.scrollable
- if self.scrollable == nil then
- self.scrollable = self.multiline
- end
- -- autoGrow defaults to true for multiline, false for single-line
- if props.autoGrow ~= nil then
- self.autoGrow = props.autoGrow
- else
- self.autoGrow = self.multiline
- end
-
- self.selectParent = nil
- self.selectOption = nil
- self._selectState = nil
-
- if type(props.selectParent) == "table" then
- self.selectParent = props.selectParent
- end
-
- if type(props.selectOption) == "table" then
- self.selectOption = props.selectOption
- end
-
- -- TextEditor creation + immediate-mode state restore is owned by the
- -- TextEditable behavior's onAttach (task 04), which runs in _attachBehaviors
- -- at the tail of Element.new (after this helper has bound self.text and the
- -- schema-driven callback fields). This subsystem-init helper no longer touches
- -- the TextEditor.
-
- -- Set parent first so it's available for size calculations
- self.parent = props.parent
-end
-
---- Special-handler binding helper for the props phase (behavior-mode-
---- unification task 08). Formerly the Element visual-state phase. Normalizes
---- border/cornerRadius/display/text/textAlign. The branch count comment below
---- refers to the 4 textAlign parse branches (table / simple-string / compound-
---- string / invalid).
-bindVisualState = function(self, props)
- local schema = Element._PropertySchema
- ------ add non-hereditary ------
- --- self drawing ---
- -- Border shape-normalization via the schema normalizer (special handler: the
- -- number-vs-table-vs-nil shape and the all-false→nil collapse are intentional).
- self.border = schema.get("border").normalizer(props.border)
- self.borderColor = props.borderColor or Element._Color.new(0, 0, 0, 1)
- self.backgroundColor = props.backgroundColor or Element._Color.new(0, 0, 0, 0)
-
- -- cornerRadius shape-normalization via the schema normalizer (special handler:
- -- number-vs-table-vs-nil and the all-zero→nil collapse are intentional).
- self.cornerRadius = schema.get("cornerRadius").normalizer(props.cornerRadius)
-
- -- display: default true; invalid (non-boolean) warns + falls back to true
- -- (non-throwing, unlike range/enum validators).
- if props.display ~= nil then
- if type(props.display) == "boolean" then
- self.display = props.display
- else
- self.display = true
- Element._ErrorHandler:warn(
- "Element",
- "ELEM_010",
- "display must be a boolean (true/false), got " .. type(props.display) .. ". Defaulting to true."
- )
- end
- else
- self.display = true
- end
-
- -- For editable elements, default text to empty string if not provided
- if self.editable and props.text == nil then
- self.text = ""
- else
- self.text = props.text
- end
-
- -- Validate and set textAlign (supports simple string, compound string, or
- -- table format). Enum membership is checked via PropertySchema validators
- -- (the valid H/V sets live there, matching the objectFit/imageRepeat pattern);
- -- compound-string parsing and warn+fallback stay here because they need
- -- ErrorHandler, which the pure-Lua schema cannot depend on.
- local textAlignMeta = schema.get("textAlign")
- local textVAlignMeta = schema.get("textAlignVertical")
- local textAlignDefault = textAlignMeta.default
- local vAlignDefault = textVAlignMeta.default
-
- self.textAlign = props.textAlign or textAlignDefault
- self.textAlignHorizontal = textAlignDefault
- self.textAlignVertical = vAlignDefault
-
- if props.textAlign ~= nil then
- if type(props.textAlign) == "table" then
- -- Table format: {horizontal = "start", vertical = "center"}
- local hAlign = props.textAlign.horizontal or textAlignDefault
- local vAlign = props.textAlign.vertical or vAlignDefault
-
- if not textAlignMeta.validator(hAlign) then
- hAlign = _warnTextAlign("textAlign.horizontal", "valid TextAlign value", hAlign, textAlignDefault)
- end
- if not textVAlignMeta.validator(vAlign) then
- vAlign = _warnTextAlign("textAlign.vertical", "valid TextAlignVertical value", vAlign, vAlignDefault)
- end
-
- self.textAlignHorizontal = hAlign
- self.textAlignVertical = vAlign
- elseif type(props.textAlign) == "string" then
- if textAlignMeta.validator(props.textAlign) then
- -- Known simple TextAlign value (backward compatible)
- self.textAlignHorizontal = props.textAlign
- self.textAlignVertical = vAlignDefault
- else
- -- Treat as compound string: "top-left" through "bottom-right"
- local parts = {}
- for part in props.textAlign:gmatch("[^-]+") do
- table.insert(parts, part)
- end
-
- if #parts == 2 then
- local verticalMap = { top = "start", center = "center", bottom = "end" }
- local horizontalMap = { left = "start", center = "center", right = "end" }
-
- local vStr = parts[1]:lower()
- local hStr = parts[2]:lower()
- local resolvedV = verticalMap[vStr]
- local resolvedH = horizontalMap[hStr]
-
- if resolvedV and resolvedH then
- self.textAlignHorizontal = resolvedH
- self.textAlignVertical = resolvedV
- else
- _warnTextAlign(
- "textAlign",
- "valid compound string (e.g., 'top-left', 'center-right')",
- props.textAlign,
- nil
- )
- end
- else
- _warnTextAlign("textAlign", "valid TextAlign value or compound string", props.textAlign, nil)
- end
- end
- end
- end
-end
-
---- Phase 5 (image + renderer init) is owned by the Themed and Imageable
---- behaviors (modules/behaviors/), attached in _attachBehaviors. Themed.onAttach
---- creates the Renderer (theme/blur config); Imageable.onAttach enriches it with
---- image config + deferred image loading. There is no longer a stub Element
---- phase for this — the behavior onAttach hooks ARE the phase
---- (behavior-mode-unification task 07/08).
-
---- Phase 6a: viewport/scale context, LayoutEngine (defaults), unit specs table,
---- fontFamily, and textSize resolution.
-function Element:_initSizingContext(props)
- --- self positioning ---
- local viewportWidth, viewportHeight = Element._Units.getViewport()
-
- ---- Sizing ----
- local gw, gh = love.window.getMode()
- self.prevGameSize = { width = gw, height = gh }
- self.autosizing = { width = false, height = false }
-
- -- Initialize LayoutEngine early with default values for auto-sizing calculations
- -- It will be re-configured later with actual layout properties
- self._layoutEngine = Element._LayoutEngine.new({
- positioning = Element._utils.enums.Positioning.RELATIVE,
- flexDirection = Element._utils.enums.FlexDirection.HORIZONTAL,
- flexWrap = Element._utils.enums.FlexWrap.NOWRAP,
- justifyContent = Element._utils.enums.JustifyContent.FLEX_START,
- alignItems = Element._utils.enums.AlignItems.STRETCH,
- alignContent = Element._utils.enums.AlignContent.STRETCH,
- gap = 0,
- gridRows = 1,
- gridColumns = 1,
-
- columnGap = 0,
- rowGap = 0,
- }, Element._layoutEngineDeps)
- self._layoutEngine:initialize(self)
-
- -- Store unit specifications for responsive behavior
- self.units = {
- width = { value = nil, unit = "px" },
- height = { value = nil, unit = "px" },
- x = { value = nil, unit = "px" },
- y = { value = nil, unit = "px" },
- textSize = { value = nil, unit = "px" },
- gap = { value = nil, unit = "px" },
- flexBasis = { value = nil, unit = "auto" },
- padding = {
- top = { value = nil, unit = "px" },
- right = { value = nil, unit = "px" },
- bottom = { value = nil, unit = "px" },
- left = { value = nil, unit = "px" },
- horizontal = { value = nil, unit = "px" }, -- Shorthand for left/right
- vertical = { value = nil, unit = "px" }, -- Shorthand for top/bottom
- },
- margin = {
- top = { value = nil, unit = "px" },
- right = { value = nil, unit = "px" },
- bottom = { value = nil, unit = "px" },
- left = { value = nil, unit = "px" },
- horizontal = { value = nil, unit = "px" }, -- Shorthand for left/right
- vertical = { value = nil, unit = "px" }, -- Shorthand for top/bottom
- },
- }
-
- local _, scaleY = Element._Context.getScaleFactors()
-
- -- minTextSize/maxTextSize/autoScaleText are bound by _applyProps (autoScaleText
- -- defaults true). They are needed before textSize processing below.
-
- -- Handle fontFamily (can be font name from theme or direct path to font file)
- -- Priority: explicit props.fontFamily > parent fontFamily > theme default
- if props.fontFamily then
- -- Explicitly set fontFamily takes highest priority
- self.fontFamily = props.fontFamily
- elseif self.parent and self.parent.fontFamily then
- -- Inherit from parent if parent has fontFamily set
- self.fontFamily = self.parent.fontFamily
- elseif props.themeComponent then
- -- If using themeComponent, try to get default from theme via ThemeManager
- local defaultFont = self._themeManager:getDefaultFontFamily()
- self.fontFamily = defaultFont and "default" or nil
- else
- self.fontFamily = nil
- end
-
- -- Handle textSize BEFORE width/height calculation (needed for auto-sizing)
- if props.textSize then
- if type(props.textSize) == "string" then
- -- Check if it's a preset first
- local presetValue, presetUnit = Element._utils.resolveTextSizePreset(props.textSize)
- local value, unit
-
- if presetValue then
- -- It's a preset, use the preset value and unit
- value, unit = presetValue, presetUnit
- self.units.textSize = { value = value, unit = unit }
- else
- -- Not a preset, parse normally
- value, unit = Element._Units.parse(props.textSize)
- self.units.textSize = { value = value, unit = unit }
- end
-
- -- Resolve textSize based on unit type
- if unit == "%" or unit == "vh" then
- -- Percentage and vh are relative to viewport height
- self.textSize = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
- elseif unit == "vw" then
- -- vw is relative to viewport width
- self.textSize = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
- elseif unit == "px" then
- -- Pixel units
- self.textSize = value
- else
- Element._ErrorHandler:error("Element", "ELEM_002", {
- unit = unit,
- })
- end
- else
- -- Validate pixel textSize value
- if props.textSize <= 0 then
- Element._ErrorHandler:error("Element", "ELEM_001", {
- value = tostring(props.textSize),
- })
- end
-
- -- Pixel textSize value
- if self.autoScaleText and Element._Context.baseScale then
- -- With base scaling: store original pixel value and scale relative to base resolution
- self.units.textSize = { value = props.textSize, unit = "px" }
- self.textSize = props.textSize * scaleY
- elseif self.autoScaleText then
- -- Without base scaling: convert to viewport units for auto-scaling
- -- Calculate what percentage of viewport height this represents
- local vhValue = (props.textSize / viewportHeight) * 100
- self.units.textSize = { value = vhValue, unit = "vh" }
- self.textSize = props.textSize -- Initial size is the specified pixel value
- else
- -- No auto-scaling: apply base scaling if set, otherwise use raw value
- self.textSize = Element._Context.baseScale and (props.textSize * scaleY) or props.textSize
- self.units.textSize = { value = props.textSize, unit = "px" }
- end
- end
- else
- -- No textSize specified - use auto-scaling default
- if self.autoScaleText and Element._Context.baseScale then
- -- With base scaling: use 12px as default and scale
- self.units.textSize = { value = 12, unit = "px" }
- self.textSize = 12 * scaleY
- elseif self.autoScaleText then
- -- Without base scaling: default to 1.5vh (1.5% of viewport height)
- self.units.textSize = { value = 1.5, unit = "vh" }
- self.textSize = (1.5 / 100) * viewportHeight
- else
- -- No auto-scaling: use 12px with optional base scaling
- self.textSize = Element._Context.baseScale and (12 * scaleY) or 12
- self.units.textSize = { value = nil, unit = "px" }
- end
- end
-end
-
---- Phase 6b: width/height/min-max/clamp, gap, flex shorthand/grow/shrink/basis,
---- 9-patch/border-box model, and padding/margin resolution + unit storage.
-function Element:_initBoxModel(props)
- local viewportWidth, viewportHeight = Element._Units.getViewport()
- local scaleX, scaleY = Element._Context.getScaleFactors()
- local _ctx = { vw = viewportWidth, vh = viewportHeight, sx = scaleX, sy = scaleY }
- -- Handle width (both w and width properties, prefer w if both exist)
- -- "auto" is treated as content-sized (same as omitting the property), per CSS semantics.
- local widthProp = props.width
- if widthProp == "auto" then
- widthProp = nil
- end
- local tempWidth -- Temporary width for padding resolution
- if widthProp then
- local parentWidth = self.parent and self.parent.width or viewportWidth
- tempWidth = _resolveUnit(self, widthProp, "width", parentWidth, _ctx, { scaleAxis = "x" })
- else
- self.autosizing.width = true
- -- Special case: if textWrap is enabled and parent exists, constrain width to parent
- -- Text wrapping requires a width constraint, so use parent's content width
- if props.textWrap and self.parent and self.parent.width then
- tempWidth = self.parent.width
- self.width = tempWidth
- self.units.width = { value = 100, unit = "%" } -- Mark as parent-constrained
- self.autosizing.width = false -- Not truly autosizing, constrained by parent
- else
- tempWidth = self:calculateAutoWidth()
- self.width = tempWidth
- self.units.width = { value = nil, unit = "auto" } -- Mark as auto-sized
- end
- end
-
- -- Handle height (both h and height properties, prefer h if both exist)
- -- "auto" is treated as content-sized (same as omitting the property), per CSS semantics.
- local heightProp = props.height
- if heightProp == "auto" then
- heightProp = nil
- end
- local tempHeight -- Temporary height for padding resolution
- if heightProp then
- local parentHeight = self.parent and self.parent.height or viewportHeight
- tempHeight = _resolveUnit(self, heightProp, "height", parentHeight, _ctx, { scaleAxis = "y" })
- else
- self.autosizing.height = true
- -- Calculate auto-height without padding first
- tempHeight = self:calculateAutoHeight()
- self.height = tempHeight
- self.units.height = { value = nil, unit = "auto" } -- Mark as auto-sized
- end
-
- local constraintParentW = self.parent and self.parent.width or viewportWidth
- local constraintParentH = self.parent and self.parent.height or viewportHeight
- _resolveUnit(self, props.minWidth, "minWidth", constraintParentW, _ctx, { scaleAxis = "x", nullable = true })
- _resolveUnit(self, props.maxWidth, "maxWidth", constraintParentW, _ctx, { scaleAxis = "x", nullable = true })
- _resolveUnit(self, props.minHeight, "minHeight", constraintParentH, _ctx, { scaleAxis = "y", nullable = true })
- _resolveUnit(self, props.maxHeight, "maxHeight", constraintParentH, _ctx, { scaleAxis = "y", nullable = true })
-
- if not self.autosizing.width then
- self.width = Element._utils.clamp(tempWidth, self.minWidth, self.maxWidth)
- tempWidth = self.width
- else
- self.width = Element._utils.clamp(self.width, self.minWidth, self.maxWidth)
- end
- if not self.autosizing.height then
- self.height = Element._utils.clamp(tempHeight, self.minHeight, self.maxHeight)
- tempHeight = self.height
- else
- self.height = Element._utils.clamp(self.height, self.minHeight, self.maxHeight)
- end
-
- --- child positioning ---
- if props.gap then
- local flexDir = props.flexDirection or Element._utils.enums.FlexDirection.HORIZONTAL
- local isHorizontalDir = flexDir == Element._utils.enums.FlexDirection.HORIZONTAL
- or flexDir == Element._utils.enums.FlexDirection.HORIZONTAL_REVERSE
- local containerSize = isHorizontalDir and self.width or self.height
- _resolveUnit(self, props.gap, "gap", containerSize, _ctx)
- else
- self.gap = 0
- self.units.gap = { value = 0, unit = "px" }
- end
-
- -- Handle flex shorthand property (sets flexGrow, flexShrink, flexBasis)
- if props.flex ~= nil then
- local grow, shrink, basis = Element._Units.parseFlexShorthand(props.flex)
-
- -- Only set individual properties if they weren't explicitly provided
- if props.flexGrow == nil then
- props.flexGrow = grow
- end
- if props.flexShrink == nil then
- props.flexShrink = shrink
- end
- if props.flexBasis == nil then
- props.flexBasis = basis
- end
- end
-
- -- Track whether flex-shrink was explicitly provided (directly or via flex shorthand)
- self._hasExplicitFlexShrink = props.flexShrink ~= nil
-
- -- Handle flexGrow property
- if props.flexGrow ~= nil then
- if type(props.flexGrow) == "number" and props.flexGrow >= 0 then
- self.flexGrow = props.flexGrow
- else
- self.flexGrow = _warnFlexInvalid(self, "FLEX_001", "flexGrow must be a non-negative number", props.flexGrow, 0)
- end
- else
- self.flexGrow = 0
- end
-
- -- Handle flexShrink property
- if props.flexShrink ~= nil then
- if type(props.flexShrink) == "number" and props.flexShrink >= 0 then
- self.flexShrink = props.flexShrink
- else
- self.flexShrink =
- _warnFlexInvalid(self, "FLEX_002", "flexShrink must be a non-negative number", props.flexShrink, 1)
- end
- else
- self.flexShrink = 1
- end
-
- -- Handle flexBasis property
- if props.flexBasis ~= nil then
- local isCalc = Element._Calc and Element._Calc.isCalc(props.flexBasis)
- if props.flexBasis == "auto" then
- self.flexBasis = "auto"
- self.units.flexBasis = { value = nil, unit = "auto" }
- elseif type(props.flexBasis) == "string" or isCalc then
- local value, unit = Element._Units.parse(props.flexBasis)
- self.units.flexBasis = { value = value, unit = unit }
- -- Don't resolve yet - LayoutEngine will handle this during layout
- self.flexBasis = props.flexBasis
- elseif type(props.flexBasis) == "number" then
- self.flexBasis = props.flexBasis
- self.units.flexBasis = { value = props.flexBasis, unit = "px" }
- else
- self.flexBasis =
- _warnFlexInvalid(self, "FLEX_003", "flexBasis must be a number, string, or 'auto'", props.flexBasis, "auto")
- self.units.flexBasis = { value = nil, unit = "auto" }
- end
- else
- self.flexBasis = "auto"
- self.units.flexBasis = { value = nil, unit = "auto" }
- end
-
- -- BORDER-BOX MODEL: For auto-sizing, we need to add padding to content dimensions
- -- For explicit sizing, width/height already include padding (border-box)
-
- -- Check if we should use 9-patch content padding for auto-sizing
- local use9PatchPadding = false
- local ninePatchContentPadding = nil
- if self._themeManager:hasThemeComponent() then
- local component = self._themeManager:getComponent()
- if component and component._ninePatchData and component._ninePatchData.contentPadding then
- -- Only use 9-patch padding if no explicit padding was provided
- if
- not props.padding
- or (
- not props.padding.top
- and not props.padding.right
- and not props.padding.bottom
- and not props.padding.left
- and not props.padding.horizontal
- and not props.padding.vertical
- )
- then
- use9PatchPadding = true
- ninePatchContentPadding = component._ninePatchData.contentPadding
- end
- end
- end
-
- -- First, resolve padding using temporary dimensions
- -- For auto-sized elements, this is content width; for explicit sizing, this is border-box width
- local tempPadding
- if use9PatchPadding then
- -- tempWidth/tempHeight are guaranteed numbers by _resolveUnit (which warns +
- -- clamps non-numbers) and calculateAutoWidth/Height; the prior defensive
- -- re-check duplicated that boundary validation (Task 11).
-
- -- Get scaled 9-patch content padding from ThemeManager
- local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight)
- if scaledPadding then
- tempPadding = scaledPadding
- else
- -- Fallback if scaling fails
- tempPadding = {
- left = ninePatchContentPadding.left,
- top = ninePatchContentPadding.top,
- right = ninePatchContentPadding.right,
- bottom = ninePatchContentPadding.bottom,
- }
- end
- else
- tempPadding = Element._Units.resolveSpacing(props.padding, self.width, self.height)
- end
-
- -- Margin percentages are relative to parent's dimensions (CSS spec)
- local parentWidth = self.parent and self.parent.width or viewportWidth
- local parentHeight = self.parent and self.parent.height or viewportHeight
- self.margin = Element._Units.resolveSpacing(props.margin, parentWidth, parentHeight)
-
- -- For auto-sized elements, add padding to get border-box dimensions
- if self.autosizing.width then
- self._borderBoxWidth = self.width + tempPadding.left + tempPadding.right
- else
- -- For explicit sizing, width is already border-box
- self._borderBoxWidth = self.width
- end
-
- if self.autosizing.height then
- self._borderBoxHeight = self.height + tempPadding.top + tempPadding.bottom
- else
- -- For explicit sizing, height is already border-box
- self._borderBoxHeight = self.height
- end
-
- -- Set final padding
- if use9PatchPadding then
- -- Use 9-patch content padding
- self.padding = {
- left = ninePatchContentPadding.left,
- top = ninePatchContentPadding.top,
- right = ninePatchContentPadding.right,
- bottom = ninePatchContentPadding.bottom,
- }
- else
- -- Re-resolve padding based on final border-box dimensions (important for percentage padding)
- self.padding = Element._Units.resolveSpacing(props.padding, self._borderBoxWidth, self._borderBoxHeight)
- end
-
- -- Calculate final content dimensions by subtracting padding from border-box
- self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right)
- self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom)
-
- -- Re-resolve textSize presets now that width/height are set
- -- (presets like "vw" need the viewport; others are resolved during constructor)
-
- -- Apply min/max constraints (also scaled)
- local minSize = self.minTextSize and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
- local maxSize = self.maxTextSize and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)
-
- if minSize and self.textSize < minSize then
- self.textSize = minSize
- end
- if maxSize and self.textSize > maxSize then
- self.textSize = maxSize
- end
-
- -- Protect against too-small text sizes (minimum 1px)
- if self.textSize < 1 then
- self.textSize = 1 -- Minimum 1px
- end
-
- -- Store original spacing values for proper resize handling
- -- Store spacing unit specs (padding + margin share identical structure)
- local sides = { "top", "right", "bottom", "left" }
- for _, kind in ipairs({ "padding", "margin" }) do
- local src = props[kind]
- if src then
- for _, axis in ipairs({ "horizontal", "vertical" }) do
- if src[axis] then
- if type(src[axis]) == "string" then
- local value, unit = Element._Units.parse(src[axis])
- self.units[kind][axis] = { value = value, unit = unit }
- else
- self.units[kind][axis] = { value = src[axis], unit = "px" }
- end
- end
- end
- end
- for _, side in ipairs(sides) do
- if src and src[side] then
- if type(src[side]) == "string" then
- local value, unit = Element._Units.parse(src[side])
- self.units[kind][side] = { value = value, unit = unit, explicit = true }
- else
- self.units[kind][side] = { value = src[side], unit = "px", explicit = true }
- end
- else
- self.units[kind][side] = { value = self[kind][side], unit = "px", explicit = false }
- end
- end
- end
-
- -- Grid properties are set later in the constructor
-end
-
---- Phase 7: hereditary positioning (no-parent and with-parent), flex/grid
---- container properties, select-frame adopt, and LayoutEngine config update.
-function Element:_initPositioning(props)
- local viewportWidth, viewportHeight = Element._Units.getViewport()
- local scaleX, scaleY = Element._Context.getScaleFactors()
- local _ctx = { vw = viewportWidth, vh = viewportHeight, sx = scaleX, sy = scaleY }
- ------ add hereditary ------
- if props.parent == nil then
- table.insert(Element._Context.topElements, self)
-
- -- Handle x position with units
- _resolveUnit(self, props.x, "x", viewportWidth, _ctx, { scaleAxis = "x", default = 0 })
-
- -- Handle y position with units
- _resolveUnit(self, props.y, "y", viewportHeight, _ctx, { scaleAxis = "y", default = 0 })
-
- self.z = Element._ZIndex.clamp(props.z or 0)
- self.tabIndex = props.tabIndex -- nil/0 = document order, >0 = explicit order, -1 = excluded from keyboard nav
-
- -- Set textColor with priority: props > theme text color > black
- if props.textColor then
- self.textColor = props.textColor
- else
- -- Try to get text color from theme via ThemeManager
- local themeToUse = self._themeManager:getTheme()
- if themeToUse and themeToUse.colors and themeToUse.colors.text then
- self.textColor = themeToUse.colors.text
- else
- -- Fallback to black
- self.textColor = Element._Color.new(0, 0, 0, 1)
- end
- end
-
- -- Track if positioning was explicitly set
- if props.positioning then
- Element._utils.validateEnum(props.positioning, Element._utils.enums.Positioning, "positioning")
- self.positioning = props.positioning
- self._originalPositioning = props.positioning
- self._explicitlyAbsolute = (props.positioning == Element._utils.enums.Positioning.ABSOLUTE)
- else
- self.positioning = Element._utils.enums.Positioning.RELATIVE
- self._originalPositioning = nil -- No explicit positioning
- self._explicitlyAbsolute = false
- end
-
- -- Handle positioning properties for elements without parent
- -- Warn if CSS positioning properties are supplied but will be ignored.
- -- Relative elements honor the offsets as visual deltas (see
- -- _applyRelativeOffsets); absolute elements use applyPositioningOffsets.
- -- Only flex-participating children (positioning coerced to ABSOLUTE but not
- -- explicitly absolute) actually drop the offsets and warrant the warning.
- if
- (props.top or props.bottom or props.left or props.right)
- and not self._explicitlyAbsolute
- and self.positioning ~= Element._utils.enums.Positioning.RELATIVE
- then
- _warnCssPositioningWithoutAbsolute(self, props)
- end
-
- -- Handle top/right/bottom/left positioning with units
- if props.top then
- _resolveUnit(self, props.top, "top", viewportHeight, _ctx)
- end
- if props.right then
- _resolveUnit(self, props.right, "right", viewportWidth, _ctx)
- end
- if props.bottom then
- _resolveUnit(self, props.bottom, "bottom", viewportHeight, _ctx)
- end
- if props.left then
- _resolveUnit(self, props.left, "left", viewportWidth, _ctx)
- end
-
- -- position: relative offsets are applied as visual deltas in
- -- LayoutEngine:layoutChildren (after the flex flow places children), so
- -- they survive the addChild -> layoutChildren re-entry here.
- else
- -- Set positioning first and track if explicitly set
- self._originalPositioning = props.positioning -- Track original intent
- if props.positioning == Element._utils.enums.Positioning.ABSOLUTE then
- self.positioning = Element._utils.enums.Positioning.ABSOLUTE
- self._explicitlyAbsolute = true -- Explicitly set to absolute by user
- elseif props.positioning == Element._utils.enums.Positioning.FLEX then
- self.positioning = Element._utils.enums.Positioning.FLEX
- self._explicitlyAbsolute = false
- elseif props.positioning == Element._utils.enums.Positioning.GRID then
- self.positioning = Element._utils.enums.Positioning.GRID
- self._explicitlyAbsolute = false
- else
- -- Default: children in flex/grid containers participate in parent's layout
- -- children in relative/absolute containers default to relative
- if
- self.parent.positioning == Element._utils.enums.Positioning.FLEX
- or self.parent.positioning == Element._utils.enums.Positioning.GRID
- then
- self.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
- self._explicitlyAbsolute = false -- Participate in parent's layout
- else
- self.positioning = Element._utils.enums.Positioning.RELATIVE
- self._explicitlyAbsolute = false -- Default for relative/absolute containers
- end
- end
-
- -- Set initial position
- local parentPadding = self.parent.padding or { left = 0, top = 0 }
- if self.positioning == Element._utils.enums.Positioning.ABSOLUTE then
- -- Absolute positioning is relative to parent's content area (padding box)
- local baseX = self.parent.x + parentPadding.left
- local baseY = self.parent.y + parentPadding.top
-
- -- Handle x/y position with units
- _resolveUnit(self, props.x, "x", self.parent.width, _ctx, { scaleAxis = "x", offset = baseX, default = 0 })
- _resolveUnit(self, props.y, "y", self.parent.height, _ctx, { scaleAxis = "y", offset = baseY, default = 0 })
-
- self.z = Element._ZIndex.clamp(props.z or 0)
- self.tabIndex = props.tabIndex
- else
- -- Children in flex containers start at parent position but will be repositioned by layoutChildren
- -- Children in absolute/relative containers start at parent's content area (accounting for padding)
- local baseX = self.parent.x + parentPadding.left
- local baseY = self.parent.y + parentPadding.top
-
- -- Warn if explicit x/y is set on a child that will be positioned by flex layout
- -- This position will be overridden unless the child has positioning="absolute"
- local parentWillUseFlex = self.parent.positioning ~= "grid"
- local childIsRelative = self.positioning ~= "absolute" or not self._explicitlyAbsolute
- if parentWillUseFlex and childIsRelative and (props.x or props.y) then
- Element._ErrorHandler:warn("Element", "LAY_008", {
- element = self.id or "unnamed",
- parent = self.parent.id or "unnamed",
- properties = (props.x and props.y) and "x, y" or (props.x and "x" or "y"),
- })
- end
-
- _resolveUnit(self, props.x, "x", self.parent.width, _ctx, { scaleAxis = "x", offset = baseX, default = 0 })
- _resolveUnit(self, props.y, "y", self.parent.height, _ctx, { scaleAxis = "y", offset = baseY, default = 0 })
-
- self.z = Element._ZIndex.clamp(props.z or self.parent.z or 0)
- self.tabIndex = props.tabIndex
- end
-
- if props.textColor then
- self.textColor = props.textColor
- elseif self.parent.textColor then
- self.textColor = self.parent.textColor
- else
- local themeToUse = self._themeManager:getTheme()
- if themeToUse and themeToUse.colors and themeToUse.colors.text then
- self.textColor = themeToUse.colors.text
- else
- -- Fallback to black
- self.textColor = Element._Color.new(0, 0, 0, 1)
- end
- end
-
- -- Handle positioning properties BEFORE adding to parent (so they're available during layout)
- -- Warn if CSS positioning properties are supplied but will be ignored.
- -- Relative elements honor the offsets as visual deltas (see
- -- _applyRelativeOffsets); absolute elements use applyPositioningOffsets.
- -- Only flex-participating children (positioning coerced to ABSOLUTE but not
- -- explicitly absolute) actually drop the offsets and warrant the warning.
- if
- (props.top or props.bottom or props.left or props.right)
- and not self._explicitlyAbsolute
- and self.positioning ~= Element._utils.enums.Positioning.RELATIVE
- then
- _warnCssPositioningWithoutAbsolute(self, props)
- end
-
- -- Handle top/right/bottom/left positioning with units
- if props.top then
- _resolveUnit(self, props.top, "top", viewportHeight, _ctx)
- end
- if props.right then
- _resolveUnit(self, props.right, "right", viewportWidth, _ctx)
- end
- if props.bottom then
- _resolveUnit(self, props.bottom, "bottom", viewportHeight, _ctx)
- end
- if props.left then
- _resolveUnit(self, props.left, "left", viewportWidth, _ctx)
- end
-
- -- position: relative offsets are applied as visual deltas in
- -- LayoutEngine:layoutChildren (after the flex flow places children), so
- -- they survive the addChild -> layoutChildren re-entry here.
-
- props.parent:addChild(self)
- end
-
- if self.positioning == Element._utils.enums.Positioning.FLEX then
- -- Validate enum properties
- if props.flexDirection then
- Element._utils.validateEnum(props.flexDirection, Element._utils.enums.FlexDirection, "flexDirection")
- end
- if props.flexWrap then
- Element._utils.validateEnum(props.flexWrap, Element._utils.enums.FlexWrap, "flexWrap")
- end
- if props.justifyContent then
- Element._utils.validateEnum(props.justifyContent, Element._utils.enums.JustifyContent, "justifyContent")
- end
- if props.alignItems then
- Element._utils.validateEnum(props.alignItems, Element._utils.enums.AlignItems, "alignItems")
- end
- if props.alignContent then
- Element._utils.validateEnum(props.alignContent, Element._utils.enums.AlignContent, "alignContent")
- end
- if props.justifySelf then
- Element._utils.validateEnum(props.justifySelf, Element._utils.enums.JustifySelf, "justifySelf")
- end
-
- -- Warn if grid properties are set with flex positioning
- if props.gridRows or props.gridColumns then
- Element._ErrorHandler:warn("Element", "LAY_010", {
- element = self.id or "unnamed",
- positioning = "flex",
- properties = "gridRows/gridColumns",
- })
- end
-
- self.flexDirection = props.flexDirection or Element._utils.enums.FlexDirection.HORIZONTAL
- self.flexWrap = props.flexWrap or Element._utils.enums.FlexWrap.NOWRAP
- self.justifyContent = props.justifyContent or Element._utils.enums.JustifyContent.FLEX_START
- self.alignItems = props.alignItems or Element._utils.enums.AlignItems.STRETCH
- self.alignContent = props.alignContent or Element._utils.enums.AlignContent.STRETCH
- self.justifySelf = props.justifySelf or Element._utils.enums.JustifySelf.AUTO
- end
-
- -- Grid container properties
- if self.positioning == Element._utils.enums.Positioning.GRID then
- -- Warn if flex properties are set with grid positioning
- if props.flexDirection or props.flexWrap or props.justifyContent then
- Element._ErrorHandler:warn("Element", "LAY_009", {
- element = self.id or "unnamed",
- positioning = "grid",
- properties = "flexDirection/flexWrap/justifyContent",
- })
- end
-
- self.gridRows = props.gridRows
- self.gridColumns = props.gridColumns
- self.alignItems = props.alignItems or Element._utils.enums.AlignItems.STRETCH
-
- -- Handle columnGap and rowGap
- _resolveUnit(self, props.columnGap, "columnGap", self.width, _ctx, { default = 0 })
- _resolveUnit(self, props.rowGap, "rowGap", self.height, _ctx, { default = 0 })
- end
-
- -- alignSelf is bound by _applyProps (default "auto").
-
- -- Update the LayoutEngine with actual layout properties
- -- (it was initialized early with defaults for auto-sizing calculations)
- self._layoutEngine.positioning = self.positioning
- if self.flexDirection then
- self._layoutEngine.flexDirection = self.flexDirection
- end
- if self.flexWrap then
- self._layoutEngine.flexWrap = self.flexWrap
- end
- if self.justifyContent then
- self._layoutEngine.justifyContent = self.justifyContent
- end
- if self.alignItems then
- self._layoutEngine.alignItems = self.alignItems
- end
- if self.alignContent then
- self._layoutEngine.alignContent = self.alignContent
- end
- if self.gap then
- self._layoutEngine.gap = self.gap
- end
- if self.gridRows then
- self._layoutEngine.gridRows = self.gridRows
- end
- if self.gridColumns then
- self._layoutEngine.gridColumns = self.gridColumns
- end
-
- if self.columnGap then
- self._layoutEngine.columnGap = self.columnGap
- end
- if self.rowGap then
- self._layoutEngine.rowGap = self.rowGap
- end
-
- -- transform is bound by _applyProps; transition is bound by _applyProps (default {}).
- -- (Previously set inline here; both are now registry-driven.)
-end
-
---- Phase 8 (ScrollManager instantiation + immediate-mode scrollbar restore) is
---- owned by the Scrollable behavior (modules/behaviors/Scrollable.lua), attached
---- in _attachBehaviors. There is no longer an Element phase for this — the
---- behavior onAttach hook IS the phase (behavior-mode-unification task 03/08).
---- `overflow` / `overflowX` / `overflowY` are bound onto the element as plain
---- fields by bindThemeAndFields/`_applyProps` so that `Element:addChild`'s
---- scroll-container auto-size guard sees them during declarative-children
---- processing in _finalizeConstruction (which runs before _attachBehaviors);
---- Scrollable.onAttach then overwrites them with the ScrollManager's normalized
---- values, matching the legacy field-exposure order.
-
---- Phase 9: immediate-mode registration, dirty flags, debug draw color,
---- declarative children tree, onCreate callback, and constructed flag.
-function Element:_finalizeConstruction(props)
- -- Register element in z-index tracking. registerElement is a mode-aware
- -- no-op outside immediate mode, so no mode check is needed here
- -- (behavior-mode-unification task 11).
- Element._Context.registerElement(self)
-
- -- Performance optimization: dirty flags for layout tracking
- -- These flags help skip unnecessary layout recalculations
- self._dirty = false -- Element properties have changed, needs layout
- self._childrenDirty = false -- Children have changed, needs layout
-
- -- Debug draw: assign a deterministic color for element boundary visualization
- -- Uses a hash of the element ID to produce a stable hue, so colors don't flash each frame
- local function hashStringToHue(str)
- local hash = 5381
- for i = 1, #str do
- hash = ((hash * 33) + string.byte(str, i)) % 360
- end
- return hash
- end
- local hue = hashStringToHue(self.id or tostring(self))
- local function hslToRgb(h)
- local s, l = 0.9, 0.55
- local c = (1 - math.abs(2 * l - 1)) * s
- local x = c * (1 - math.abs((h / 60) % 2 - 1))
- local m = l - c / 2
- local r, g, b
- if h < 60 then
- r, g, b = c, x, 0
- elseif h < 120 then
- r, g, b = x, c, 0
- elseif h < 180 then
- r, g, b = 0, c, x
- elseif h < 240 then
- r, g, b = 0, x, c
- elseif h < 300 then
- r, g, b = x, 0, c
- else
- r, g, b = c, 0, x
- end
- return r + m, g + m, b + m
- end
- local dr, dg, db = hslToRgb(hue)
- self._debugColor = { dr, dg, db }
-
- -- Process declarative children prop: build child tree from property tables
- -- Placed after all self properties are initialized so children can safely access parent state
- if props.children then
- if type(props.children) ~= "table" then
- _warnChildrenInvalid(self, "ELEM_010", "children must be a table array", props.children)
- else
- for i = 1, #props.children do
- local childProps = props.children[i]
- if childProps == nil then
- _warnChildrenInvalid(self, "ELEM_011", "nil entry in children array, skipping", nil)
- elseif type(childProps) ~= "table" then
- _warnChildrenInvalid(self, "ELEM_012", "non-table entry in children array, skipping", childProps)
- else
- local childCopy = {}
- for k, v in pairs(childProps) do
- childCopy[k] = v
- end
- childCopy.parent = self
- local child = Element.new(childCopy)
-
- -- Set up state management for declarative children so mutations
- -- made in event callbacks persist across frames. Mode-aware via
- -- StateManager.isImmediateMode (behavior-mode-unification task 11):
- -- this whole block is immediate-mode-only frame bookkeeping.
- if Element._StateManager.isImmediateMode() then
- if not child.id or child.id == "" then
- child.id = Element._StateManager.generateID(childCopy, self)
- end
- local childState = Element._StateManager.getState(child.id, {})
- Element._StateManager.markStateUsed(child.id)
- child:restoreState(childState)
- child._stateId = child.id
-
- -- Restore theme state from event handler state
- if child.themeComponent then
- local eventState = childState.eventHandler or {}
- if child.disabled or eventState.disabled then
- child._themeState = "disabled"
- elseif child.active or eventState.active then
- child._themeState = "active"
- elseif eventState._pressed and next(eventState._pressed) then
- child._themeState = "pressed"
- elseif eventState._hovered then
- child._themeState = "hover"
- else
- child._themeState = "normal"
- end
- end
-
- -- Add to current frame elements for saveState tracking
- if Element._Context._currentFrameElements then
- table.insert(Element._Context._currentFrameElements, child)
- end
- end
- end
- end
- end
- end
-
- -- Fire onCreate callback if provided
- if self.onCreate then
- if self.onCreateDeferred then
- local FlexLove = package.loaded["FlexLove"] or package.loaded["libs.FlexLove"]
- if FlexLove and FlexLove.deferCallback then
- FlexLove.deferCallback(function()
- self.onCreate(self, props)
- end)
- else
- self.onCreate(self, props)
- end
- else
- self.onCreate(self, props)
- end
- end
-
- -- Mark element as fully constructed.
- -- NOTE: no longer gates an __newindex dimension warning (removed — see comment
- -- at top of file). Retained lazily in case future write-interception is added.
- self._constructed = true
-end
-
---- Retrieve the element's screen-space rectangle for collision detection and positioning calculations
---- Use this for custom layout logic, tooltips, or detecting overlaps between elements
----@return { x:number, y:number, width:number, height:number }
-function Element:getBounds()
- return { x = self.x, y = self.y, width = self:getBorderBoxWidth(), height = self:getBorderBoxHeight() }
-end
-
---- Test if a screen coordinate falls within the element's clickable area
---- Use this for custom hit detection or determining which element the mouse is over
---- @param x number
---- @param y number
---- @return boolean
-function Element:contains(x, y)
- local bounds = self:getBounds()
- return bounds.x <= x and bounds.y <= y and bounds.x + bounds.width >= x and bounds.y + bounds.height >= y
-end
-
---- Get the element's total width including padding for layout calculations
---- Use this when you need the full visual width rather than just content width
----@return number
-function Element:getBorderBoxWidth()
- return self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
-end
-
---- Get the element's total height including padding for layout calculations
---- Use this when you need the full visual height rather than just content height
----@return number
-function Element:getBorderBoxHeight()
- return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
-end
-
---- Get computed box dimensions (content area position and size)
---- Returns the position and size of the content area (inside padding)
----@return {x: number, y: number, width: number, height: number}
-function Element:getComputedBox()
- return {
- x = self.x + self.padding.left,
- y = self.y + self.padding.top,
- width = self.width,
- height = self.height,
- }
-end
-
---- Mark this element and its ancestors as dirty, requiring layout recalculation
---- Call this when element properties change that affect layout
-function Element:invalidateLayout()
- self._dirty = true
-
- -- Invalidate dimension caches
- self._borderBoxWidthCache = nil
- self._borderBoxHeightCache = nil
-
- -- Mark parent as having dirty children
- if self.parent then
- self.parent._childrenDirty = true
- -- Propagate up the tree (parents need to know their descendants changed)
- local ancestor = self.parent
- while ancestor do
- ancestor._childrenDirty = true
- ancestor = ancestor.parent
- end
- end
-end
-
--- Scroll / scrollbar methods (_syncScrollManagerState, _detectOverflow, setScrollPosition,
--- _calculateScrollbarDimensions, _getScrollbarAtPosition, _handleScrollbarPress/Drag/Release,
--- _handleWheelScroll, getScrollPosition, getMaxScroll, getScrollPercentage, hasOverflow,
--- getContentSize, scrollBy, scrollToTop) are bound to ScrollManager in Element.init. ScrollManager
--- owns all scrollbar interaction logic; Element retains only 1-line delegates (see ScrollManager.lua).
-
---- Mark a method for deferred retry during the update phase.
---- Methods that depend on layout calculations (e.g., scroll, sizing)
---- can defer themselves when preconditions aren't met. They'll be
---- retried automatically each frame in update() until they succeed.
----@param methodName string The method name to retry
----@param ... any? Arguments to forward on retry
-function Element:_deferMethod(methodName, ...)
- if type(self[methodName]) ~= "function" then
- Element._ErrorHandler:warn("Element", "CORE_005", {
- element = self.id,
- method = tostring(methodName),
- })
- return
- end
-
- if #self._deferredMethods >= MAX_DEFERRED_METHODS then
- Element._ErrorHandler:warn("Element", "CORE_004", {
- element = self.id,
- method = tostring(methodName),
- retryCount = MAX_DEFERRED_METHODS,
- })
- return
- end
-
- local argc = select("#", ...)
- local args = {}
- for i = 1, argc do
- local val = select(i, ...)
- args[i] = val == nil and _DEFERRED_NIL or val
- end
- table.insert(self._deferredMethods, {
- methodName = methodName,
- args = args,
- argc = argc,
- retryCount = 0,
- })
-end
-
--- Deferred image loading is owned by the Imageable behavior
--- (modules/behaviors/Imageable.lua). Imageable.onAttach installs an instance
--- closure on `element._loadImage` and defers it via _deferMethod; the deferred-
--- method dispatcher (which resolves `self[methodName]`) invokes that closure.
--- Element no longer owns the load logic itself and has zero image-branch logic.
--- (behavior-mode-unification task 07)
-
--- scrollToBottom / scrollToLeft / scrollToRight are bound to ScrollManager in Element.init.
-
---- Get the current state's scaled content padding
---- Returns the contentPadding for the current theme state, scaled to the element's size
----@return table|nil -- {left, top, right, bottom} or nil if no contentPadding
-function Element:getScaledContentPadding()
- local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
- local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
- return self._themeManager:getScaledContentPadding(borderBoxWidth, borderBoxHeight)
-end
-
---- Get draw-time content offset from state-specific theme padding changes
----@return number offsetX, number offsetY
-function Element:getContentStateOffset()
- local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
- local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
-
- local currentPadding = self:getScaledContentPadding()
- local basePadding = self._themeManager:_getScaledContentPaddingForState("normal", borderBoxWidth, borderBoxHeight)
-
- if not currentPadding or not basePadding then
- return 0, 0
- end
-
- local offsetX = currentPadding.left - basePadding.left
- local offsetY = currentPadding.top - basePadding.top
-
- if math.abs(offsetX) < 0.001 then
- offsetX = 0
- end
- if math.abs(offsetY) < 0.001 then
- offsetY = 0
- end
-
- return offsetX, offsetY
-end
-
---- Get or create blur instance for this element
----@return table? -- Blur instance or nil if no blur configured
-function Element:getBlurInstance()
- -- Determine quality from contentBlur or backdropBlur
- local quality = 5 -- Default quality
- if self.contentBlur and self.contentBlur.quality then
- quality = self.contentBlur.quality
- elseif self.backdropBlur and self.backdropBlur.quality then
- quality = self.backdropBlur.quality
- end
-
- -- Create blur instance if needed
- if not self._blurInstance or self._blurInstance.quality ~= quality then
- self._blurInstance = Element._Blur.new({ quality = quality })
- end
-
- return self._blurInstance
-end
-
---- Get available content width for children (accounting for 9-patch content padding)
---- This is the width that children should use when calculating percentage widths
----@return number
-function Element:getAvailableContentWidth()
- local availableWidth = self.width
-
- local scaledContentPadding = self:getScaledContentPadding()
- if scaledContentPadding then
- -- Check if the element is using the scaled 9-patch contentPadding as its padding
- -- Allow small floating point differences (within 0.1 pixels)
- local usingContentPaddingAsPadding = (
- math.abs(self.padding.left - scaledContentPadding.left) < 0.1
- and math.abs(self.padding.right - scaledContentPadding.right) < 0.1
- )
-
- if not usingContentPaddingAsPadding then
- -- Element has explicit padding different from contentPadding
- -- Subtract scaled contentPadding to get the area children should use
- availableWidth = availableWidth - scaledContentPadding.left - scaledContentPadding.right
- end
- end
-
- return math.max(0, availableWidth)
-end
-
---- Get available content height for children (accounting for 9-patch content padding)
---- This is the height that children should use when calculating percentage heights
----@return number
-function Element:getAvailableContentHeight()
- local availableHeight = self.height
-
- local scaledContentPadding = self:getScaledContentPadding()
- if scaledContentPadding then
- -- Check if the element is using the scaled 9-patch contentPadding as its padding
- -- Allow small floating point differences (within 0.1 pixels)
- local usingContentPaddingAsPadding = (
- math.abs(self.padding.top - scaledContentPadding.top) < 0.1
- and math.abs(self.padding.bottom - scaledContentPadding.bottom) < 0.1
- )
-
- if not usingContentPaddingAsPadding then
- -- Element has explicit padding different from contentPadding
- -- Subtract scaled contentPadding to get the area children should use
- availableHeight = availableHeight - scaledContentPadding.top - scaledContentPadding.bottom
- end
- end
-
- return math.max(0, availableHeight)
-end
-
-function Element:openSelect()
- Element._Select.openSelect(self)
-end
-
-function Element:closeSelect()
- Element._Select.closeSelect(self)
-end
-
-function Element:toggleSelect()
- Element._Select.toggleSelect(self)
-end
-
----@return boolean
-function Element:isSelectOpen()
- return Element._Select.isSelectOpen(self)
-end
-
----@return any
-function Element:getSelectValue()
- return Element._Select.getSelectValue(self)
-end
-
----@return string?
-function Element:getSelectLabel()
- return Element._Select.getSelectLabel(self)
-end
-
----@return boolean
-function Element:isSelectedSelectOption()
- return Element._Select.isSelectedOption(self)
-end
-
----@param value any
----@param optionElement Element?
-function Element:setSelectValue(value, optionElement)
- Element._Select.setSelectValue(self, value, optionElement)
-end
-
-function Element:_handleSelectRelease()
- Element._Select.handleRelease(self)
-end
-
---- Dynamically insert a child element into the hierarchy for runtime UI construction
---- Use this to build interfaces procedurally or add elements based on application state
----@param child Element
-function Element:addChild(child)
- if self._managedSelectFrame and child.selectOption and self._managedSelectOwner then
- child._selectParentHint = self._managedSelectOwner
- end
-
- child.parent = self
-
- -- Re-evaluate positioning now that we have a parent
- -- If child was created without explicit positioning, inherit from parent
- if child._originalPositioning == nil then
- -- No explicit positioning was set during construction
- if
- self.positioning == Element._utils.enums.Positioning.FLEX
- or self.positioning == Element._utils.enums.Positioning.GRID
- then
- child.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
- child._explicitlyAbsolute = false -- Participate in parent's layout
- else
- child.positioning = Element._utils.enums.Positioning.RELATIVE
- child._explicitlyAbsolute = false -- Default for relative/absolute containers
- end
- end
- -- If child._originalPositioning is set, it means explicit positioning was provided
- -- and _explicitlyAbsolute was already set correctly during construction
-
- table.insert(self.children, child)
- Element._Select.registerWithSelectParent(child)
-
- -- Mark parent as having dirty children to trigger layout recalculation
- self._childrenDirty = true
-
- -- Only recalculate auto-sizing if the child participates in layout
- -- (CSS: absolutely positioned children don't affect parent auto-sizing)
- if not child._explicitlyAbsolute then
- local sizeChanged = false
-
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
- local isScrollContainer = overflowX == "scroll"
- or overflowX == "auto"
- or overflowY == "scroll"
- or overflowY == "auto"
-
- if self.autosizing.height and not isScrollContainer then
- local oldHeight = self.height
- local contentHeight = self:calculateAutoHeight()
- -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
- self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
- self.height = contentHeight
- if oldHeight ~= self.height then
- sizeChanged = true
- end
- end
- if self.autosizing.width and not isScrollContainer then
- local oldWidth = self.width
- local contentWidth = self:calculateAutoWidth()
- -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
- self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
- self.width = contentWidth
- if oldWidth ~= self.width then
- sizeChanged = true
- end
- end
-
- -- Propagate size change up the tree
- if sizeChanged and self.parent and (self.parent.autosizing.width or self.parent.autosizing.height) then
- -- Trigger parent to recalculate its size by re-adding this child's contribution
- -- This ensures grandparents are notified of size changes
- if self.parent.autosizing.height then
- local contentHeight = self.parent:calculateAutoHeight()
- self.parent._borderBoxHeight = contentHeight + self.parent.padding.top + self.parent.padding.bottom
- self.parent.height = contentHeight
- end
- if self.parent.autosizing.width then
- local contentWidth = self.parent:calculateAutoWidth()
- self.parent._borderBoxWidth = contentWidth + self.parent.padding.left + self.parent.padding.right
- self.parent.width = contentWidth
- end
- end
- end
-
- -- Layout is deferred to FlexLove.endFrame in immediate mode (all elements
- -- for the frame must exist before layout). shouldLayout() encapsulates the
- -- mode check (behavior-mode-unification task 11).
- if Element._StateManager.shouldLayout() then
- self:layoutChildren()
- end
-
- if
- self._selectState
- and self._selectState.selectFrame
- and child.selectOption
- and child ~= self._selectState.selectFrame
- then
- Element._Select.attachOptionToManagedFrame(child)
- end
-end
-
---- Remove a child element from the hierarchy to dynamically update UIs
---- Use this to delete elements when they're no longer needed or respond to user actions
----@param child Element
-function Element:removeChild(child)
- for i, c in ipairs(self.children) do
- if c == child then
- Element._Select.handleChildRemoved(self, child)
- Element._Select.unregisterFromSelectParent(child)
- table.remove(self.children, i)
- child.parent = nil
-
- -- Recalculate auto-sizing if needed
- if self.autosizing.width or self.autosizing.height then
- if self.autosizing.width then
- local contentWidth = self:calculateAutoWidth()
- self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
- self.width = contentWidth
- end
- if self.autosizing.height then
- local contentHeight = self:calculateAutoHeight()
- self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
- self.height = contentHeight
- end
- end
-
- -- Re-layout children after removal (deferred in immediate mode).
- if Element._StateManager.shouldLayout() then
- self:layoutChildren()
- end
-
- break
- end
- end
-end
-
---- Reparent this element to a new parent, properly detaching from the current location
---- and inserting into the new parent's children hierarchy with correct layout and alignment.
---- If newParent is nil, the element becomes a top-level element.
---- Works whether the element was originally created with or without a parent.
----@param newParent Element?
-function Element:setParent(newParent)
- local expectedManagedSelectParent = nil
- if self._managedSelectFrame and self._managedSelectOwner then
- expectedManagedSelectParent = self._managedSelectOwner
- if self._managedSelectOwner._selectState and self._managedSelectOwner._selectState.selectAnchor then
- expectedManagedSelectParent = self._managedSelectOwner._selectState.selectAnchor
- end
- end
-
- if self._managedSelectFrame and self._managedSelectOwner and newParent ~= expectedManagedSelectParent then
- Element._Select.warnSelectFrame(self._managedSelectOwner, "ELEM_009", {
- element = self._managedSelectOwner.id,
- frame = self.id,
- expectedParent = expectedManagedSelectParent and expectedManagedSelectParent.id or nil,
- actualParent = newParent and newParent.id or nil,
- })
- end
-
- if self.parent == newParent then
- return -- Already at this parent, no-op
- end
-
- -- Remove from current location
- if self.parent then
- -- removeChild sets child.parent = nil and recalculates parent layout
- self.parent:removeChild(self)
- else
- -- Remove from topElements (element was created without a parent)
- for i, elem in ipairs(Element._Context.topElements) do
- if elem == self then
- table.remove(Element._Context.topElements, i)
- break
- end
- end
- self.parent = nil
- end
-
- if newParent then
- -- addChild handles: setting self.parent, re-evaluating positioning,
- -- inserting into children, marking dirty, auto-sizing, and layoutChildren
- newParent:addChild(self)
- else
- -- Become a top-level element
- self.parent = nil
- self.x = self.x or 0
- self.y = self.y or 0
- self.z = Element._ZIndex.clamp(self.z or 0)
- table.insert(Element._Context.topElements, self)
- end
-end
-
---- Delete all child elements at once for resetting containers or clearing lists
---- Use this to efficiently empty containers when rebuilding UI from scratch
-function Element:clearChildren()
- -- Clear parent references for all children
- for _, child in ipairs(self.children) do
- Element._Select.unregisterFromSelectParent(child)
- child.parent = nil
- end
-
- -- Clear the children table
- self.children = {}
-
- -- Recalculate auto-sizing if needed
- if self.autosizing.width or self.autosizing.height then
- if self.autosizing.width then
- local contentWidth = self:calculateAutoWidth()
- self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
- self.width = contentWidth
- end
- if self.autosizing.height then
- local contentHeight = self:calculateAutoHeight()
- self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
- self.height = contentHeight
- end
- end
-
- -- Re-layout (though there are no children now; deferred in immediate mode).
- if Element._StateManager.shouldLayout() then
- self:layoutChildren()
- end
-end
-
---- Get the number of children this element has
----@return number
-function Element:getChildCount()
- return #self.children
-end
-
---- Apply positioning offsets (top, right, bottom, left) to an element
--- @param element The element to apply offsets to
-function Element:applyPositioningOffsets(element)
- -- Delegate to LayoutEngine
- self._layoutEngine:applyPositioningOffsets(element)
-end
-
-function Element:layoutChildren()
- -- Check performance warnings (only on root elements to avoid spam)
- if not self.parent then
- self:_checkPerformanceWarnings()
- end
-
- -- Catch stale bare dimension writes that bypassed setProperty (e.g.
- -- `element.width = "42%"` stores a raw string). Lua __newindex cannot intercept
- -- these at write time (the keys exist post-construction), so we validate lazily
- -- here, once per element per property, only when a reflow is already pending.
- if self._dirty then
- self:_checkDimensionTypes()
- end
-
- -- Delegate layout to LayoutEngine
- self._layoutEngine:layoutChildren()
-end
-
---- Warn once per stale dimension property that holds a non-number value, which
---- indicates a bare write (e.g. `element.width = "42%"`) bypassed setProperty.
---- Bare dimension writes neither resolve unit strings nor invalidate layout;
---- the element renders with the wrong size until :setProperty() is used.
-function Element:_checkDimensionTypes()
- if not self._dimWarned then
- self._dimWarned = {}
- end
- for _, prop in ipairs({ "width", "height", "x", "y" }) do
- local v = self[prop]
- if v ~= nil and type(v) ~= "number" then
- if not self._dimWarned[prop] then
- self._dimWarned[prop] = true
- Element._ErrorHandler:warn("Element", "ELM_001", {
- property = prop,
- message = string.format(
- 'element.%s holds a non-number value (%s); a bare write bypassed setProperty and was not resolved to pixels. Use element:setProperty("%s", value) instead.',
- prop,
- type(v),
- prop
- ),
- })
- end
- end
- end
-end
-
---- Warn about percentage sizing with auto-sizing parent
----@param child Element
----@param axis string "width" or "height"
-function Element:_warnIfPercentageWithAutoSizing(child, axis)
- if self._managedSelectFrame then
- return
- end
- Element._ErrorHandler:warn("LayoutEngine", "LAY_004", {
- child = child.id or "unnamed",
- issue = "percentage " .. axis .. " with parent auto-sizing",
- })
-end
-
---- Whether element needs cross-axis percentage dimension syncing
---- Managed select frames sync percentage children with container dimensions
----@return boolean
-function Element:_shouldSyncPercentageDimensions()
- return self._managedSelectFrame == true
-end
-
---- Adjust cross-axis percentage width for managed select minimum
----@param child Element
----@param newBorderBoxWidth number
----@return number
-function Element:_adjustCrossAxisPercentageWidth(child, newBorderBoxWidth)
- if self._managedSelectFrame and self.autosizing and self.autosizing.width then
- local intrinsicBorderBoxWidth = child:calculateAutoWidth() + child.padding.left + child.padding.right
- return math.max(newBorderBoxWidth, intrinsicBorderBoxWidth)
- end
- return newBorderBoxWidth
-end
-
---- Layout-path delegate: adjust child border-box width for a managed-select frame.
---- Owned by Select; routed through here so the layout path stays free of dropdown details.
----@param child Element
----@param childBorderBoxWidth number
----@return number
-function Element:_adjustAutoWidthChildBorderBoxForManagedSelect(child, childBorderBoxWidth)
- return Element._Select.adjustAutoWidthChild(self, child, childBorderBoxWidth)
-end
-
---- Destroy element and its children
-function Element:destroy()
- -- Remove from global elements list
- for i, win in ipairs(Element._Context.topElements) do
- if win == self then
- table.remove(Element._Context.topElements, i)
- break
- end
- end
-
- if self.parent then
- for i, child in ipairs(self.parent.children) do
- if child == self then
- Element._Select.unregisterFromSelectParent(self)
- table.remove(self.parent.children, i)
- break
- end
- end
- self.parent = nil
- end
-
- -- Destroy all children
- for _, child in ipairs(self.children) do
- child:destroy()
- end
-
- -- Clear children table
- self.children = {}
-
- -- Clear parent reference
- if self.parent then
- self.parent = nil
- end
-
- -- Clear animation reference
- self.animation = nil
-
- -- Clear onEvent to prevent closure leaks
- self.onEvent = nil
-
- -- Clear touch callbacks to prevent closure leaks
- self.onTouchEvent = nil
- self.onGesture = nil
-
- Element._Select.cleanupDestroy(self)
-end
-
---- Retry deferred methods queued via `_deferMethod` during this frame. Each
---- pending entry is invoked through pcall; failures are reported to the
---- ErrorHandler instead of aborting the frame, and entries that re-defer are
---- retried next frame with an incremented retry count up to MAX_DEFER_RETRIES.
---- Extracted from the tail of Element:update so update stays a thin
---- behavior-dispatch orchestrator (behavior-mode-unification task 09).
-function Element:_processDeferredMethods()
- if #self._deferredMethods == 0 then
- return
- end
- local pending = self._deferredMethods
- self._deferredMethods = {}
- for _, entry in ipairs(pending) do
- if entry.retryCount >= MAX_DEFER_RETRIES then
- Element._ErrorHandler:warn("Element", "CORE_004", {
- element = self.id,
- method = tostring(entry.methodName),
- retryCount = entry.retryCount,
- })
- else
- local beforeCount = #self._deferredMethods
- local callArgs = {}
- for j = 1, entry.argc do
- local val = entry.args[j]
- if val == _DEFERRED_NIL then
- callArgs[j] = nil
- else
- callArgs[j] = val
- end
- end
- local success, err = pcall(function()
- self[entry.methodName](self, unpack(callArgs, 1, entry.argc))
- end)
- if not success then
- Element._ErrorHandler:warn("Element", "CORE_002", {
- element = self.id,
- method = tostring(entry.methodName),
- error = tostring(err),
- })
- end
- -- Propagate retry count to any new deferred entry for the same method
- for i = beforeCount + 1, #self._deferredMethods do
- if self._deferredMethods[i].methodName == entry.methodName then
- self._deferredMethods[i].retryCount = entry.retryCount + 1
- end
- end
- end
- end
-end
-
---- Draw element and its children
-function Element:draw(backdropCanvas)
- -- Early exit if element is display:none or invisible (optimization)
- if self.display == false or self.opacity <= 0 or self.visibility == "hidden" then
- return
- end
-
- -- Background behaviors (drawLayer ~= "overlay") render BEFORE children in
- -- registry order: Themed (core Renderer:draw), Clickable (pressed overlay),
- -- ... Overlay behaviors (Scrollable scrollbars) render AFTER children below.
- local drawCtx = { backdropCanvas = backdropCanvas }
- local behaviors = self.behaviors
- for i = 1, #behaviors do
- local b = behaviors[i]
- if b.drawLayer ~= "overlay" then
- b.onDraw(self, drawCtx)
- end
- end
-
- -- Core child hierarchy rendering (clipping, sorting, scroll offset, blur).
- -- Stays in Element: it is structural, not a per-capability behavior.
- self:_drawChildren(backdropCanvas)
-
- -- Overlay behaviors (drawLayer == "overlay") render AFTER children so they
- -- paint on top, e.g. Scrollable's scrollbars (behavior-mode-unification 09).
- for i = 1, #behaviors do
- local b = behaviors[i]
- if b.drawLayer == "overlay" then
- b.onDraw(self, drawCtx)
- end
- end
-end
-
---- Core child-drawing pipeline extracted from Element:draw so the draw entry
---- point stays a thin behavior-dispatch orchestrator (task 09). Owns z-sort,
---- rounded-corner/overflow clipping (stencil > scissor), scroll/content offset,
---- optional content-blur application, and recursive child:draw. Not a behavior
---- — this is structural hierarchy rendering shared by every element.
-function Element:_drawChildren(backdropCanvas)
- local sortedChildren = {}
- for _, child in ipairs(self.children) do
- table.insert(sortedChildren, child)
- end
- if #sortedChildren == 0 then
- return
- end
- table.sort(sortedChildren, function(a, b)
- return a.z < b.z
- end)
-
- local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
- local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
-
- -- Check if we need to clip children to rounded corners
- local hasRoundedCorners = false
- if self.cornerRadius then
- if type(self.cornerRadius) == "number" then
- hasRoundedCorners = self.cornerRadius > 0
- else
- hasRoundedCorners = self.cornerRadius.topLeft > 0
- or self.cornerRadius.topRight > 0
- or self.cornerRadius.bottomLeft > 0
- or self.cornerRadius.bottomRight > 0
- end
- end
-
- -- Render the (possibly clipped + offset) child layer, applying content blur
- -- when configured. The inner closure performs clipping/offset/draw; blur
- -- wraps it in a region pass when a blur instance is available.
- local function renderChildLayer()
- local contentOffsetX, contentOffsetY = self:getContentStateOffset()
-
- -- Determine overflow behavior per axis (matches HTML/CSS behavior)
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
- local needsOverflowClipping = (overflowX ~= "visible" or overflowY ~= "visible")
- and (overflowX ~= nil or overflowY ~= nil)
-
- -- Apply scroll/content offset after clipping is set
- local hasScrollOffset = needsOverflowClipping and (self._scrollX ~= 0 or self._scrollY ~= 0)
- local hasContentOffset = contentOffsetX ~= 0 or contentOffsetY ~= 0
- local hasOffset = hasScrollOffset or hasContentOffset
-
- -- Set up clipping: rounded-corners (stencil) > overflow (scissor) > none
- local clipMode = "none"
- if hasRoundedCorners then
- local roundedBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
- local roundedBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
- local stencilFunc =
- Element._RoundedRect.stencilFunction(self.x, self.y, roundedBoxWidth, roundedBoxHeight, self.cornerRadius)
- local currentCanvas = love.graphics.getCanvas()
- love.graphics.setCanvas()
- love.graphics.stencil(stencilFunc, "replace", 1)
- love.graphics.setCanvas(currentCanvas)
- love.graphics.setStencilTest("greater", 0)
- clipMode = "stencil"
- elseif needsOverflowClipping then
- love.graphics.setScissor(self.x + self.padding.left, self.y + self.padding.top, self.width, self.height)
- clipMode = "scissor"
- end
-
- if hasOffset then
- love.graphics.push()
- love.graphics.translate(
- (hasScrollOffset and -self._scrollX or 0) + contentOffsetX,
- (hasScrollOffset and -self._scrollY or 0) + contentOffsetY
- )
- end
-
- for _, child in ipairs(sortedChildren) do
- child:draw(backdropCanvas)
- end
-
- if hasOffset then
- love.graphics.pop()
- end
-
- -- Restore clipping state
- if clipMode == "stencil" then
- love.graphics.setStencilTest()
- elseif clipMode == "scissor" then
- love.graphics.setScissor()
- end
- end
-
- -- Apply content blur if configured
- if self.contentBlur and self.contentBlur.radius > 0 then
- local blurInstance = self:getBlurInstance()
- if blurInstance then
- Element._Blur.applyToRegion(
- blurInstance,
- self.contentBlur.radius,
- self.x,
- self.y,
- borderBoxWidth,
- borderBoxHeight,
- renderChildLayer
- )
- else
- renderChildLayer()
- end
- else
- renderChildLayer()
- end
-end
-
---- Update element (propagate to children)
----@param dt number
-function Element:update(dt)
- if self.display == false then
- return
- end
- if not self.parent then
- self:_trackActiveAnimations()
- end
- for _, child in ipairs(self.children) do
- child:update(dt)
- end
- -- Advance direct-assignment animations before the loop so geometry is current
- -- for hit-testing; no-ops when the Animated behavior is already attached.
- Element._dispatchAnimatedUpdate(self, dt)
- for _, b in ipairs(self.behaviors) do
- b.onUpdate(self, dt)
- end
- self:_processDeferredMethods()
-end
-
---- Handle a touch event directly (for external touch routing)
---- Invokes both onEvent and onTouchEvent callbacks if set
----@param touchEvent InputEvent The touch event to handle
-function Element:handleTouchEvent(touchEvent)
- if not self.touchEnabled or self.disabled then
- return
- end
- if self._eventHandler then
- self._eventHandler:_invokeCallback(self, touchEvent)
- self._eventHandler:_invokeTouchCallback(self, touchEvent)
- end
-end
-
---- Handle a gesture event (from GestureRecognizer or external routing)
----@param gesture table The gesture data (type, position, velocity, etc.)
-function Element:handleGesture(gesture)
- if not self.touchEnabled or self.disabled then
- return
- end
- if self._eventHandler then
- self._eventHandler:_invokeGestureCallback(self, gesture)
- end
-end
-
---- Get active touches currently tracked on this element
----@return table Active touches keyed by touch ID
-function Element:getTouches()
- if self._eventHandler then
- return self._eventHandler:getActiveTouches()
- end
- return {}
-end
-
----@param newViewportWidth number
----@param newViewportHeight number
-function Element:recalculateUnits(newViewportWidth, newViewportHeight)
- self._layoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
-end
-
---- Resize element and its children based on game window size change
----@param newGameWidth number
----@param newGameHeight number
-function Element:resize(newGameWidth, newGameHeight)
- self:recalculateUnits(newGameWidth, newGameHeight)
- self:_refreshSizeConstraints(newGameWidth, newGameHeight)
-
- -- For non-auto-sized elements with viewport/percentage units, update content dimensions from border-box
- if not self.autosizing.width and self._borderBoxWidth and self.units.width.unit ~= "px" then
- self._borderBoxWidth = Element._utils.clamp(self._borderBoxWidth, self.minWidth, self.maxWidth)
- self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right)
- end
- if not self.autosizing.height and self._borderBoxHeight and self.units.height.unit ~= "px" then
- self._borderBoxHeight = Element._utils.clamp(self._borderBoxHeight, self.minHeight, self.maxHeight)
- self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom)
- end
-
- -- Update children
- for _, child in ipairs(self.children) do
- child:resize(newGameWidth, newGameHeight)
- end
-
- -- Recalculate auto-sized dimensions after children are resized
- if self.autosizing.width then
- local contentWidth = self:calculateAutoWidth()
- -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
- self._borderBoxWidth =
- Element._utils.clamp(contentWidth + self.padding.left + self.padding.right, self.minWidth, self.maxWidth)
- self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right)
- -- CONTENT-LEVEL CLAMP: CSS min-width/max-width also bound the content width.
- -- Subtracting padding from the clamped border-box can drop the content width
- -- below minWidth (e.g. minWidth=200, horizontal padding=100 => content=100),
- -- so re-clamp the content dimension with the shared size-clamping utility.
- self.width = Element._utils.clampSize(self.width, self.minWidth, self.maxWidth)
- end
- if self.autosizing.height then
- local contentHeight = self:calculateAutoHeight()
- -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
- self._borderBoxHeight =
- Element._utils.clamp(contentHeight + self.padding.top + self.padding.bottom, self.minHeight, self.maxHeight)
- self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom)
- -- CONTENT-LEVEL CLAMP: CSS min-height/max-height also bound the content height.
- -- Subtracting padding from the clamped border-box can drop the content height
- -- below minHeight (e.g. minHeight=200, vertical padding=100 => content=100),
- -- so re-clamp the content dimension with the shared size-clamping utility.
- self.height = Element._utils.clampSize(self.height, self.minHeight, self.maxHeight)
- end
-
- -- Re-resolve textSize if it uses viewport-relative units after dimensions are finalized
-
- self:layoutChildren()
- self.prevGameSize.width = newGameWidth
- self.prevGameSize.height = newGameHeight
-end
-
-function Element:_refreshSizeConstraints(newViewportWidth, newViewportHeight)
- local scaleX, scaleY = Element._Context.getScaleFactors()
- local ctx = { vw = newViewportWidth, vh = newViewportHeight, sx = scaleX, sy = scaleY }
- local parentW = self.parent and self.parent.width or newViewportWidth
- local parentH = self.parent and self.parent.height or newViewportHeight
- _refreshUnit(self, "minWidth", parentW, ctx, "x")
- _refreshUnit(self, "maxWidth", parentW, ctx, "x")
- _refreshUnit(self, "minHeight", parentH, ctx, "y")
- _refreshUnit(self, "maxHeight", parentH, ctx, "y")
-end
-
---- Calculate text width for button
----@return number
-function Element:calculateTextWidth()
- if self.text == nil then
- return 0
- end
-
- local font = Element._utils.getFont(self.textSize, self.fontFamily, self.themeComponent, self._themeManager)
- local width = font:getWidth(self.text)
- return Element._utils.applyContentMultiplier(width, self.contentAutoSizingMultiplier, "width")
-end
-
----@return number
-function Element:calculateTextHeight()
- if self.text == nil then
- return 0
- end
-
- local font = Element._utils.getFont(self.textSize, self.fontFamily, self.themeComponent, self._themeManager)
- local height = font:getHeight()
-
- if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then
- local availableWidth = self.width
-
- if (not availableWidth or availableWidth <= 0) and self.parent then
- availableWidth = self.parent.width
- end
-
- if availableWidth and availableWidth > 0 then
- local _, wrappedLines = font:getWrap(self.text, availableWidth)
- height = height * #wrappedLines
- end
- end
-
- return Element._utils.applyContentMultiplier(height, self.contentAutoSizingMultiplier, "height")
-end
-
-function Element:calculateAutoWidth()
- local contentWidth = self._layoutEngine:calculateAutoWidth()
- if self._managedSelectMinimumBorderBoxWidth then
- local minimumContentWidth =
- math.max(0, self._managedSelectMinimumBorderBoxWidth - self.padding.left - self.padding.right)
- contentWidth = math.max(contentWidth, minimumContentWidth)
- end
- return contentWidth
-end
-
---- Calculate auto height based on children
-function Element:calculateAutoHeight()
- return self._layoutEngine:calculateAutoHeight()
-end
-
----@param newText string
----@param autoresize boolean? --default: false
-function Element:updateText(newText, autoresize)
- self.text = newText or self.text
- if autoresize then
- self.width = self:calculateTextWidth()
- self.height = self:calculateTextHeight()
- end
-end
-
----@param newOpacity number
-function Element:updateOpacity(newOpacity)
- self.opacity = newOpacity
- for _, child in ipairs(self.children) do
- child:updateOpacity(newOpacity)
- end
-end
-
---- same as calling updateOpacity(0)
-function Element:hide()
- self:updateOpacity(0)
-end
-
---- same as calling updateOpacity(1)
-function Element:show()
- self:updateOpacity(1)
-end
-
--- ====================
--- Input Handling - Text Editing (behavior-delegated, task 04)
--- ====================
--- All text-editor operations are dispatched through the TextEditable behavior
--- (modules/behaviors/TextEditable.lua). Element retains only thin 1-line
--- forwarders for backward-compat with external callers (EventHandler,
--- KeyboardNavigation, Renderer, game UI). The behavior owns the TextEditor
--- subsystem (onAttach creates it, onUpdate drives cursor blink, saveState /
--- restoreState persist it) AND implements the delegate bodies (text sync,
--- auto-grow, nil-guarding element._textEditor) — so Element carries zero
--- text-editor nil-guard branches and zero text-editor logic.
---
--- `_wrapLine` / `_getFont` remain here: they are RENDERER forwarders (not
--- TextEditor delegates), and the TextEditor has its own implementations.
--- `updateText` (above) is a plain-label text setter, not a TextEditor delegate.
--- ====================
-
---- Set cursor position (delegates to TextEditable behavior)
----@param position number -- Character index (0-based)
-function Element:setCursorPosition(position)
- return Element._TextEditable.setCursorPosition(self, position)
-end
-
---- Get cursor position (delegates to TextEditable behavior)
----@return number -- Character index (0-based)
-function Element:getCursorPosition()
- return Element._TextEditable.getCursorPosition(self)
-end
-
---- Move cursor by delta characters (delegates to TextEditable behavior)
----@param delta number -- Number of characters to move (positive or negative)
-function Element:moveCursorBy(delta)
- return Element._TextEditable.moveCursorBy(self, delta)
-end
-
---- Move cursor to start of text (delegates to TextEditable behavior)
-function Element:moveCursorToStart()
- return Element._TextEditable.moveCursorToStart(self)
-end
-
---- Move cursor to end of text (delegates to TextEditable behavior)
-function Element:moveCursorToEnd()
- return Element._TextEditable.moveCursorToEnd(self)
-end
-
---- Move cursor to start of current line (delegates to TextEditable behavior)
-function Element:moveCursorToLineStart()
- return Element._TextEditable.moveCursorToLineStart(self)
-end
-
---- Move cursor to end of current line (delegates to TextEditable behavior)
-function Element:moveCursorToLineEnd()
- return Element._TextEditable.moveCursorToLineEnd(self)
-end
-
---- Move cursor to start of previous word (delegates to TextEditable behavior)
-function Element:moveCursorToPreviousWord()
- return Element._TextEditable.moveCursorToPreviousWord(self)
-end
-
---- Move cursor to start of next word (delegates to TextEditable behavior)
-function Element:moveCursorToNextWord()
- return Element._TextEditable.moveCursorToNextWord(self)
-end
-
---- Set selection range (delegates to TextEditable behavior)
----@param startPos number -- Start position (inclusive)
----@param endPos number -- End position (inclusive)
-function Element:setSelection(startPos, endPos)
- return Element._TextEditable.setSelection(self, startPos, endPos)
-end
-
---- Get selection range (delegates to TextEditable behavior)
----@return number?, number? -- Start and end positions, or nil if no selection
-function Element:getSelection()
- return Element._TextEditable.getSelection(self)
-end
-
---- Check if there is an active selection (delegates to TextEditable behavior)
----@return boolean
-function Element:hasSelection()
- return Element._TextEditable.hasSelection(self)
-end
-
---- Clear selection (delegates to TextEditable behavior)
-function Element:clearSelection()
- return Element._TextEditable.clearSelection(self)
-end
-
---- Select all text (delegates to TextEditable behavior)
-function Element:selectAll()
- return Element._TextEditable.selectAll(self)
-end
-
---- Get selected text (delegates to TextEditable behavior)
----@return string? -- Selected text or nil if no selection
-function Element:getSelectedText()
- return Element._TextEditable.getSelectedText(self)
-end
-
---- Delete selected text (delegates to TextEditable behavior, which owns text sync + auto-grow)
----@return boolean -- True if text was deleted
-function Element:deleteSelection()
- return Element._TextEditable.deleteSelection(self)
-end
-
---- Give this element keyboard focus to enable text input or keyboard navigation
---- Use this to automatically focus text fields when showing forms or dialogs
-function Element:focus()
- return Element._TextEditable.focus(self)
-end
-
---- Remove keyboard focus to stop capturing input events
---- Use this when closing popups or switching focus to other elements
-function Element:blur()
- return Element._TextEditable.blur(self)
-end
-
---- Query focus state to conditionally render focus indicators or handle keyboard input
---- Use this to style focused elements or determine which element receives keyboard events
----@return boolean
-function Element:isFocused()
- return Element._TextEditable.isFocused(self)
-end
-
---- Retrieve the element's current text content for processing or validation
---- Use this to read user input from text fields or get display text
----@return string
-function Element:getText()
- return Element._TextEditable.getText(self)
-end
-
---- Update the element's text content programmatically for dynamic labels or resetting inputs
---- Use this to change text without user input, like clearing fields or updating status messages
----@param text string
-function Element:setText(text)
- return Element._TextEditable.setText(self, text)
-end
-
---- Programmatically insert text at any position for autocomplete or text manipulation
---- Use this to implement suggestions, templates, or text snippets
----@param text string -- Text to insert
----@param position number? -- Position to insert at (default: cursor position)
-function Element:insertText(text, position)
- return Element._TextEditable.insertText(self, text, position)
-end
-
----@param startPos number -- Start position (inclusive)
----@param endPos number -- End position (inclusive)
-function Element:deleteText(startPos, endPos)
- return Element._TextEditable.deleteText(self, startPos, endPos)
-end
-
---- Replace text in range
----@param startPos number -- Start position (inclusive)
----@param endPos number -- End position (inclusive)
----@param newText string -- Replacement text
-function Element:replaceText(startPos, endPos, newText)
- return Element._TextEditable.replaceText(self, startPos, endPos, newText)
-end
-
---- Wrap a single line of text
----@param line string -- Line to wrap
----@param maxWidth number -- Maximum width in pixels
----@return table -- Array of wrapped line parts
-function Element:_wrapLine(line, maxWidth)
- return self._renderer:wrapLine(self, line, maxWidth)
-end
-
----@return love.Font
-function Element:_getFont()
- return self._renderer:getFont(self)
-end
-
--- ====================
--- Input Handling - Mouse Selection
--- ====================
-
---- Handle mouse click on text (set cursor position or start selection)
---- Delegates to the TextEditable behavior, which owns drag tracking.
----@param mouseX number -- Mouse X coordinate
----@param mouseY number -- Mouse Y coordinate
----@param clickCount number -- Number of clicks (1=single, 2=double, 3=triple)
-function Element:_handleTextClick(mouseX, mouseY, clickCount)
- return Element._TextEditable._handleTextClick(self, mouseX, mouseY, clickCount)
-end
-
---- Handle mouse drag for text selection
---- Delegates to the TextEditable behavior, which owns drag tracking.
----@param mouseX number -- Mouse X coordinate
----@param mouseY number -- Mouse Y coordinate
-function Element:_handleTextDrag(mouseX, mouseY)
- return Element._TextEditable._handleTextDrag(self, mouseX, mouseY)
-end
-
--- ====================
--- Input Handling - Keyboard Input (behavior-delegated, task 04)
--- ====================
-
---- Handle text input (character input) — delegates to the TextEditable behavior.
----@param text string -- Character(s) to insert
-function Element:textinput(text)
- return Element._TextEditable.textinput(self, text)
-end
-
---- Handle key press (special keys) — delegates to the TextEditable behavior.
----@param key string -- Key name
----@param scancode string -- Scancode
----@param isrepeat boolean -- Whether this is a key repeat
-function Element:keypressed(key, scancode, isrepeat)
- return Element._TextEditable.keypressed(self, key, scancode, isrepeat)
-end
-
--- ====================
--- Performance Monitoring
--- ====================
-
---- Get hierarchy depth of this element
----@return number depth Depth in the element tree (0 for root)
-function Element:getHierarchyDepth()
- local depth = 0
- local current = self.parent
- while current do
- depth = depth + 1
- current = current.parent
- end
- return depth
-end
-
---- Count total elements in this tree
----@return number count Total number of elements including this one and all descendants
-function Element:countElements()
- local count = 1 -- Count self
- for _, child in ipairs(self.children) do
- count = count + child:countElements()
- end
- return count
-end
-
-function Element:_checkPerformanceWarnings()
- if not Element._Performance or not Element._Performance.warningsEnabled then
- return
- end
-
- -- Check hierarchy depth
- local depth = self:getHierarchyDepth()
- if depth >= 15 then
- Element._Performance:logWarning(
- string.format("hierarchy_depth_%s", self.id),
- "Element",
- string.format("Element hierarchy depth is %d levels for element '%s'", depth, self.id or "unnamed"),
- { depth = depth, elementId = self.id or "unnamed" },
- "Deep nesting can impact performance. Consider flattening the structure or using absolute positioning"
- )
- end
-
- -- Check total element count (only for root elements)
- if not self.parent then
- local totalElements = self:countElements()
- if totalElements >= 1000 then
- Element._Performance:logWarning(
- "element_count_high",
- "Element",
- string.format("UI contains %d+ elements", totalElements),
- { elementCount = totalElements },
- "Large element counts may impact performance. Consider virtualization for long lists or pagination for large datasets"
- )
- end
- end
-end
-
---- Count active animations in tree
----@return number count Number of active animations
-function Element:_countActiveAnimations()
- local count = self.animation and 1 or 0
- for _, child in ipairs(self.children) do
- count = count + child:_countActiveAnimations()
- end
- return count
-end
-
---- Track active animations and warn if too many
-function Element:_trackActiveAnimations()
- -- Get Performance instance from deps if available
- if not Element._Performance or not Element._Performance.warningsEnabled then
- return
- end
-
- local animCount = self:_countActiveAnimations()
- if animCount >= 50 then
- Element._Performance:logWarning(
- "animation_count_high",
- "Element",
- string.format("%d+ animations running simultaneously", animCount),
- { animationCount = animCount },
- "High animation counts may impact frame rate. Consider reducing concurrent animations or using CSS-style transitions"
- )
- end
-end
-
---- Change the tint color of an image element dynamically for hover effects or state indication
---- Use this to recolor images without replacing the asset, like highlighting selected items
----@param color Color Color to tint the image
-function Element:setImageTint(color)
- self.imageTint = color
-end
-
---- Adjust image transparency independently from the element for fade effects
---- Use this to create image-specific fade animations or disabled states
----@param opacity number Opacity 0-1
-function Element:setImageOpacity(opacity)
- if opacity ~= nil then
- Element._utils.validateRange(opacity, 0, 1, "imageOpacity")
- end
- self.imageOpacity = opacity
-end
-
---- Set image repeat mode
----@param repeatMode string Repeat mode: "no-repeat", "repeat", "repeat-x", "repeat-y", "space", "round"
-function Element:setImageRepeat(repeatMode)
- local validImageRepeat = {
- ["no-repeat"] = "no-repeat",
- ["repeat"] = "repeat",
- ["repeat-x"] = "repeat-x",
- ["repeat-y"] = "repeat-y",
- space = "space",
- round = "round",
- }
- Element._utils.validateEnum(repeatMode, validImageRepeat, "imageRepeat")
- self.imageRepeat = repeatMode
-end
-
---- Apply rotation transform to create spinning animations or rotated layouts
---- Use this for loading spinners, compass needles, or angled UI elements
----@param angle number Angle in radians
-function Element:rotate(angle)
- if not self.transform then
- self.transform = Element._Transform.new({})
- end
- self.transform.rotate = angle
-end
-
---- Resize element visually using scale transforms for zoom effects
---- Use this for hover magnification, shrinking animations, or responsive scaling
----@param scaleX number X-axis scale
----@param scaleY number? Y-axis scale (defaults to scaleX)
-function Element:scale(scaleX, scaleY)
- if not self.transform then
- self.transform = Element._Transform.new({})
- end
- self.transform.scaleX = scaleX
- self.transform.scaleY = scaleY or scaleX
-end
-
---- Offset element position using transforms for smooth movement without layout recalculation
---- Use this for parallax effects, draggable elements, or position animations
----@param x number X translation
----@param y number Y translation
-function Element:translate(x, y)
- if not self.transform then
- self.transform = Element._Transform.new({})
- end
- self.transform.translateX = x
- self.transform.translateY = y
-end
-
---- Define the pivot point for rotation and scaling transforms
---- Use this to rotate around corners, edges, or custom points rather than the center
----@param originX number X origin (0-1, where 0.5 is center)
----@param originY number Y origin (0-1, where 0.5 is center)
-function Element:setTransformOrigin(originX, originY)
- if not self.transform then
- self.transform = Element._Transform.new({})
- end
- self.transform.originX = originX
- self.transform.originY = originY
-end
-
---- Animate element to new property values with automatic transition
---- Captures current values as start, uses provided values as final, and applies the animation
----@param props table Target property values
----@param duration number? Animation duration in seconds (default: 0.3)
----@param easing string? Easing function name (default: "linear")
----@return Element self For method chaining
-function Element:animateTo(props, duration, easing)
- if not Element._Animation then
- _warnAnimApi("ELEM_003")
- return self
- end
-
- if type(props) ~= "table" then
- _warnAnimApi("ELEM_003")
- return self
- end
-
- duration = duration or 0.3
- easing = easing or "linear"
-
- -- Collect current values as start
- local startValues = {}
- for key, _ in pairs(props) do
- startValues[key] = self[key]
- end
-
- -- Create and apply animation
- local anim = Element._Animation.new({
- duration = duration,
- start = startValues,
- final = props,
- easing = easing,
- })
-
- anim:apply(self)
- return self
-end
-
---- Fade element to full opacity
----@param duration number? Duration in seconds (default: 0.3)
----@param easing string? Easing function name
----@return Element self For method chaining
-function Element:fadeIn(duration, easing)
- return self:animateTo({ opacity = 1 }, duration or 0.3, easing)
-end
-
---- Fade element to zero opacity
----@param duration number? Duration in seconds (default: 0.3)
----@param easing string? Easing function name
----@return Element self For method chaining
-function Element:fadeOut(duration, easing)
- return self:animateTo({ opacity = 0 }, duration or 0.3, easing)
-end
-
---- Scale element to target scale value using transforms
----@param targetScale number Target scale multiplier
----@param duration number? Duration in seconds (default: 0.3)
----@param easing string? Easing function name
----@return Element self For method chaining
-function Element:scaleTo(targetScale, duration, easing)
- if not Element._Animation or not Element._Transform then
- _warnAnimApi("ELEM_003")
- return self
- end
-
- -- Ensure element has a transform
- if not self.transform then
- self.transform = Element._Transform.new({})
- end
-
- local currentScaleX = self.transform.scaleX or 1
- local currentScaleY = self.transform.scaleY or 1
-
- local anim = Element._Animation.new({
- duration = duration or 0.3,
- start = { scaleX = currentScaleX, scaleY = currentScaleY },
- final = { scaleX = targetScale, scaleY = targetScale },
- easing = easing or "linear",
- })
-
- anim:apply(self)
- return self
-end
-
---- Move element to target position
----@param x number Target x position
----@param y number Target y position
----@param duration number? Duration in seconds (default: 0.3)
----@param easing string? Easing function name
----@return Element self For method chaining
-function Element:moveTo(x, y, duration, easing)
- return self:animateTo({ x = x, y = y }, duration or 0.3, easing)
-end
-
---- Set transition configuration for a property
----@param property string Property name or "all" for all properties
----@param config table Transition config {duration, easing, delay, onComplete}
-function Element:setTransition(property, config)
- if not self.transitions then
- self.transitions = {}
- end
-
- if type(config) ~= "table" then
- _warnAnimApi("ELEM_003")
- config = {}
- end
-
- -- Validate config
- if config.duration and (type(config.duration) ~= "number" or config.duration < 0) then
- _warnAnimApi("ELEM_004", config.duration)
- config.duration = 0.3
- end
-
- self.transitions[property] = {
- duration = config.duration or 0.3,
- easing = config.easing or "easeOutQuad",
- delay = config.delay or 0,
- onComplete = config.onComplete,
- }
-end
-
---- Set transition configuration for multiple properties
----@param groupName string Name for this transition group
----@param config table Transition config {duration, easing, delay, onComplete}
----@param properties table Array of property names
-function Element:setTransitionGroup(_, config, properties)
- if type(properties) ~= "table" then
- _warnAnimApi("ELEM_005")
- return
- end
-
- for _, prop in ipairs(properties) do
- self:setTransition(prop, config)
- end
-end
-
---- Remove transition configuration for a property
----@param property string Property name or "all" to remove all
-function Element:removeTransition(property)
- if not self.transitions then
- return
- end
-
- if property == "all" then
- self.transitions = {}
- else
- self.transitions[property] = nil
- end
-end
-
---- Resolve a unit-based dimension property (width/height) from a string or CalcObject
---- Parses the value, updates self.units, resolves to pixels, and updates border-box dimensions
----@param property string "width" or "height"
----@param value string|table The unit string (e.g., "50%", "10vw") or CalcObject
----@return number resolvedValue The resolved pixel value
-function Element:_resolveDimensionProperty(property, value)
- local viewportWidth, viewportHeight = Element._Units.getViewport()
- local parsedValue, parsedUnit = Element._Units.parse(value)
- self.units[property] = { value = parsedValue, unit = parsedUnit }
-
- local parentDimension
- if property == "width" then
- parentDimension = self.parent and self.parent.width or viewportWidth
- else
- parentDimension = self.parent and self.parent.height or viewportHeight
- end
-
- local resolved = Element._Units.resolve(parsedValue, parsedUnit, viewportWidth, viewportHeight, parentDimension)
-
- if type(resolved) ~= "number" then
- Element._ErrorHandler:warn("Element", "LAY_003", {
- issue = string.format("%s resolution returned non-number value", property),
- type = type(resolved),
- value = tostring(resolved),
- })
- resolved = 0
- end
-
- self[property] = resolved
-
- if property == "width" then
- if self.autosizing and self.autosizing.width then
- self._borderBoxWidth = resolved + self.padding.left + self.padding.right
- else
- self._borderBoxWidth = resolved
- end
- else
- if self.autosizing and self.autosizing.height then
- self._borderBoxHeight = resolved + self.padding.top + self.padding.bottom
- else
- self._borderBoxHeight = resolved
- end
- end
-
- return resolved
-end
-
---- Resolve a dimension (width/height) prop given a unit-string/Calc value.
---- Handles the unit-sameness short-circuit, transition-on-resolved-pixel-value
---- semantics, and layout invalidation. Exits setProperty (caller returns).
-local function _setDimensionWithUnit(self, property, value, transitionConfig)
- -- Check if the unit specification is the same (compare against stored units)
- local currentUnits = self.units[property]
- local newValue, newUnit = Element._Units.parse(value)
- if currentUnits and currentUnits.value == newValue and currentUnits.unit == newUnit then
- return
- end
-
- if transitionConfig then
- -- For transitions, resolve the target value and transition the pixel value
- local currentPixelValue = self[property]
- local resolvedTarget = self:_resolveDimensionProperty(property, value)
-
- if currentPixelValue ~= nil and currentPixelValue ~= resolvedTarget then
- -- Reset to current value before animating
- self[property] = currentPixelValue
- local Animation = require("modules.Animation")
- local anim = Animation.new({
- duration = transitionConfig.duration,
- start = { [property] = currentPixelValue },
- final = { [property] = resolvedTarget },
- easing = transitionConfig.easing,
- onComplete = transitionConfig.onComplete,
- })
- anim:apply(self)
- end
- else
- self:_resolveDimensionProperty(property, value)
- end
-
- self:invalidateLayout()
-end
-
---- Apply a transition animation from the current value to `value` for `property`.
---- Falls back to a direct write when there is no current value to animate from.
-local function _animatePropertyTo(self, property, value, transitionConfig)
- local currentValue = self[property]
- if currentValue ~= nil then
- local Animation = require("modules.Animation")
- local anim = Animation.new({
- duration = transitionConfig.duration,
- start = { [property] = currentValue },
- final = { [property] = value },
- easing = transitionConfig.easing,
- onComplete = transitionConfig.onComplete,
- })
- anim:apply(self)
- else
- self[property] = value
- end
-end
-
--- Explicit handler map for the few props with genuinely-different setProperty
--- semantics that cannot be expressed via schema flags alone. Adding a new prop
--- with ordinary behavior requires NO new entry here — it flows through the
--- generic flagged dispatch below. Handlers signal a full-handled early return.
-local _specialSetHandlers = {
- parent = function(self, value)
- self:setParent(value)
- return true
- end,
- themeComponent = function(self, value)
- self.themeComponent = value
- self:_syncThemeAndRenderer("themeComponent", value)
- return true
- end,
- -- imagePath / image: setting these must re-run the Imageable load pipeline
- -- (recompute `_loadedImage`, fire onImageLoad/onImageError, defer I/O). The
- -- Imageable behavior installs `element._reloadImage` at construction; if it
- -- is absent the element has no image concern (Imageable only attaches when
- -- imagePath/image is declared at construction), so the field is set but no
- -- load occurs — late image-concern acquisition requires re-attaching the
- -- behavior, which is outside the attach-at-construction contract.
- imagePath = function(self, value)
- self.imagePath = value
- if self._reloadImage then
- self:_reloadImage()
- end
- return true
- end,
- image = function(self, value)
- self.image = value
- if self._reloadImage then
- self:_reloadImage()
- end
- return true
- end,
-}
-
---- Set property with automatic transition.
---- Dispatch is registry-driven: dimension/unit props route through
---- `_setDimensionWithUnit`, the genuinely-special props (parent,
---- themeComponent, imagePath, image) route through `_specialSetHandlers`, and
---- everything else is a single generic path that consults schema flags
---- (`affectsLayout` / `syncsTheme`) for layout invalidation and theme sync. No
---- inline hardcoded property-name branches and no per-call table allocation.
----@param property string Property name
----@param value any New value
-function Element:setProperty(property, value)
- local transitionConfig
- if self.transitions then
- transitionConfig = self.transitions[property] or self.transitions["all"]
- end
-
- local schema = Element._PropertySchema
-
- -- 1. Dimension prop with a unit string / CalcObject: resolve to pixels.
- if schema.isDimension(property) and (type(value) == "string" or (Element._Calc and Element._Calc.isCalc(value))) then
- _setDimensionWithUnit(self, property, value, transitionConfig)
- return
- end
-
- -- 2. Genuinely-special props (parent reparenting, themeComponent sync).
- local handler = _specialSetHandlers[property]
- if handler then
- handler(self, value)
- return
- end
-
- -- 3. Generic flagged dispatch.
- -- Skip write/transition/layout work for unchanged values, but still sync
- -- theme state: disabled/active must reach setThemeState even when the value is
- -- unchanged (renderer/theme state may have been reset out-of-band).
- if self[property] ~= value then
- if transitionConfig then
- _animatePropertyTo(self, property, value, transitionConfig)
- else
- self[property] = value
- end
- if schema.affectsLayout(property) then
- self:invalidateLayout()
- end
- end
- if schema.syncsTheme(property) then
- self:_syncThemeAndRenderer(property, value)
- end
-end
-
----Sync ThemeManager and Renderer when properties change that affect rendering
----@param property string The property name that changed
----@param value any The new value
-function Element:_syncThemeAndRenderer(property, value)
- -- Visual props (backgroundColor/borderColor/cornerRadius/opacity) and callbacks
- -- (onEvent/onTouchEvent/onGesture) are intentionally NOT synced here: Renderer:draw
- -- and EventHandler dispatch read them from the element as source of truth, so a
- -- bare `element. = v` write is immediately consistent with setProperty(...).
- -- Only stateful side effects (theme-state machine + themeManager component) remain.
- if property == "disabled" then
- if self._themeManager then
- self._themeManager.disabled = value
- end
- if self._renderer then
- self._renderer:setThemeState(value and "disabled" or "normal")
- end
- elseif property == "active" then
- if self._themeManager then
- self._themeManager.active = value
- end
- if self._renderer then
- self._renderer:setThemeState(value and "active" or "normal")
- end
- elseif property == "themeComponent" then
- if self._themeManager then
- self._themeManager.themeComponent = value
- end
- end
-end
-
--- ====================
--- State Persistence (behavior-mode-unification task 12)
--- ====================
-
---- Save all element state for immediate-mode persistence.
---- Each attached behavior owns its own state extraction (saveState hook) and
---- returns a snapshot (or nil) merged into the consolidated state table:
---- Clickable → `eventHandler`, Scrollable → `scrollManager`, TextEditable →
---- `textEditor` + drag tracking, Selectable → `select`, Themed → `blur`,
---- Persistable → `_props` (public scalar mutations). Element owns ZERO
---- per-subsystem extraction logic — this method is a pure dispatch loop.
----@return ElementStateData state Complete state snapshot
-function Element:saveState()
- local state = {}
- for i = 1, #self.behaviors do
- local bstate = self.behaviors[i].saveState(self)
- if bstate ~= nil then
- for k, v in pairs(bstate) do
- state[k] = v
- end
- end
- end
- return state
-end
-
---- Restore all element state from StateManager.
---- Each attached behavior owns its own hydration (restoreState hook) and reads
---- only its own slice from the full state table. Registry order places
---- Persistable last so `_props` overrides subsystem-hydrated state, preserving
---- the legacy restore ordering. Element owns ZERO per-subsystem hydration.
----@param state ElementStateData State to restore
-function Element:restoreState(state)
- if not state then
- return
- end
- for i = 1, #self.behaviors do
- self.behaviors[i].restoreState(self, state)
- end
-end
-
---- Cleanup method to break circular references (immediate-mode frame end).
---- Iterates each attached behavior's `onDetach` hook so every behavior tears
---- down what its `onAttach` created (Clickable releases the EventHandler,
---- TextEditable the TextEditor, Themed the Renderer, Selectable the select
---- fields, Imageable the image callbacks), then clears the behaviors list and
---- unregisters from StateManager. Does NOT clear onEvent / onTouchEvent /
---- onGesture — the Renderer/EventHandler read those directly from the element
---- (not the cache), so clearing them here would break retained mode.
-function Element:_cleanup()
- for i = 1, #self.behaviors do
- self.behaviors[i].onDetach(self)
- end
- self.behaviors = {}
- -- onCreate fires once at construction (already invoked by now); release it.
- self.onCreate = nil
- if self._stateId and self._stateId ~= "" then
- Element._StateManager.unregisterStateful(self._stateId)
- end
-end
-
--- ====================
--- Keyboard Navigation
--- ====================
-
---- Check if this element can receive keyboard focus
----@return boolean
-function Element:isFocusable()
- if self.disabled then
- return false
- end
- -- Capability query: an element is keyboard-focusable when it is editable, has
- -- an event/text handler, participates in the Select subsystem, or is a
- -- touch-interactive element with callbacks. Expressed as a single boolean
- -- expression (not a dispatch branch) because focusability is a query, not a
- -- per-frame behavior.
- return not not (
- self.editable
- or type(self.onEvent) == "function"
- or self._selectState
- or self.selectOption
- or self.onTextInput
- or (self.touchEnabled and (self.onTouchEvent or self.onGesture))
- )
-end
-
---- Get all focusable children in DOM/document order (depth-first traversal)
---- Elements are collected in the order they appear in the children array,
---- with nested children collected after their parent. This matches standard
---- browser tab order behavior where elements are ordered by document position.
----@return Element[]
-function Element:getFocusableChildren()
- local focusable = {}
-
- local function collectFocusable(elem)
- for _, child in ipairs(elem.children) do
- -- Check self first
- if child:isFocusable() then
- table.insert(focusable, child)
- end
-
- -- Then recurse (depth-first)
- collectFocusable(child)
- end
- end
-
- collectFocusable(self)
- return focusable
-end
-
---- Get next focusable element in sequence
----@param container Element The container element
----@param currentElement Element? Current focused element
----@param wrap boolean? Whether to wrap around
----@return Element?
-function Element.getNextFocusable(container, currentElement, wrap)
- local focusable = container:getFocusableChildren()
- if #focusable == 0 then
- return nil
- end
-
- -- Find current index
- local currentIndex = 0
- if currentElement then
- for i, elem in ipairs(focusable) do
- if elem == currentElement then
- currentIndex = i
- break
- end
- end
- end
-
- -- Find next
- local nextIndex = currentIndex + 1
- if nextIndex > #focusable then
- if wrap then
- nextIndex = 1
- else
- return nil
- end
- end
-
- return focusable[nextIndex]
-end
-
---- Get previous focusable element in sequence
----@param container Element The container element
----@param currentElement Element? Current focused element
----@param wrap boolean? Whether to wrap around
----@return Element?
-function Element.getPreviousFocusable(container, currentElement, wrap)
- local focusable = container:getFocusableChildren()
- if #focusable == 0 then
- return nil
- end
-
- -- Find current index
- local currentIndex = #focusable + 1
- if currentElement then
- for i, elem in ipairs(focusable) do
- if elem == currentElement then
- currentIndex = i
- break
- end
- end
- end
-
- -- Find previous
- local prevIndex = currentIndex - 1
- if prevIndex < 1 then
- if wrap then
- prevIndex = #focusable
- else
- return nil
- end
- end
-
- return focusable[prevIndex]
-end
-
-return Element
diff --git a/libs/flexlove/modules/Enums.lua b/libs/flexlove/modules/Enums.lua
deleted file mode 100644
index 9e94b4c1..00000000
--- a/libs/flexlove/modules/Enums.lua
+++ /dev/null
@@ -1,171 +0,0 @@
--- Layout, flex, text, image, and ARIA enums used across FlexLove.
--- Extracted from utils so utils stays under its LOC budget; re-exported as
--- `utils.enums` for backward compatibility.
-
-local enums = {
- ---@enum TextAlign
- TextAlign = { START = "start", CENTER = "center", END = "end", JUSTIFY = "justify" },
- ---@enum TextAlignVertical
- TextAlignVertical = { START = "start", CENTER = "center", END = "end" },
- ---@enum Positioning
- Positioning = { ABSOLUTE = "absolute", RELATIVE = "relative", FLEX = "flex", GRID = "grid" },
- ---@enum FlexDirection
- FlexDirection = {
- HORIZONTAL = "horizontal",
- VERTICAL = "vertical",
- ROW = "row",
- COLUMN = "column",
- HORIZONTAL_REVERSE = "horizontal-reverse",
- VERTICAL_REVERSE = "vertical-reverse",
- ROW_REVERSE = "row-reverse",
- COLUMN_REVERSE = "column-reverse",
- },
- ---@enum JustifyContent
- JustifyContent = {
- FLEX_START = "flex-start",
- CENTER = "center",
- SPACE_AROUND = "space-around",
- FLEX_END = "flex-end",
- SPACE_EVENLY = "space-evenly",
- SPACE_BETWEEN = "space-between",
- },
- ---@enum JustifySelf
- JustifySelf = {
- AUTO = "auto",
- FLEX_START = "flex-start",
- CENTER = "center",
- FLEX_END = "flex-end",
- SPACE_AROUND = "space-around",
- SPACE_EVENLY = "space-evenly",
- SPACE_BETWEEN = "space-between",
- },
- ---@enum AlignItems
- AlignItems = {
- STRETCH = "stretch",
- FLEX_START = "flex-start",
- FLEX_END = "flex-end",
- CENTER = "center",
- BASELINE = "baseline",
- },
- ---@enum AlignSelf
- AlignSelf = {
- AUTO = "auto",
- STRETCH = "stretch",
- FLEX_START = "flex-start",
- FLEX_END = "flex-end",
- CENTER = "center",
- BASELINE = "baseline",
- },
- ---@enum AlignContent
- AlignContent = {
- STRETCH = "stretch",
- FLEX_START = "flex-start",
- FLEX_END = "flex-end",
- CENTER = "center",
- SPACE_BETWEEN = "space-between",
- SPACE_AROUND = "space-around",
- },
- ---@enum FlexWrap
- FlexWrap = { NOWRAP = "nowrap", WRAP = "wrap", WRAP_REVERSE = "wrap-reverse" },
- ---@enum TextSize
- TextSize = {
- XXS = "xxs",
- XS = "xs",
- SM = "sm",
- MD = "md",
- LG = "lg",
- XL = "xl",
- XXL = "xxl",
- XL3 = "3xl",
- XL4 = "4xl",
- },
- ---@enum ImageRepeat
- ImageRepeat = {
- NO_REPEAT = "no-repeat",
- REPEAT = "repeat",
- REPEAT_X = "repeat-x",
- REPEAT_Y = "repeat-y",
- SPACE = "space",
- ROUND = "round",
- },
-
- ---@enum ARIA Role (accessibility roles for screen readers)
- ARIA = {
- -- Widget roles
- BUTTON = "button",
- CHECKBOX = "checkbox",
- LINK = "link",
- MENUITEM = "menuitem",
- MENUITEMCHECKBOX = "menuitemcheckbox",
- MENUITEMRADIO = "menuitemradio",
- PROGRESSBAR = "progressbar",
- RADIO = "radio",
- SCROLLBAR = "scrollbar",
- SLIDER = "slider",
- SPINBUTTON = "spinbutton",
- SWITCH = "switch",
- TAB = "tab",
- TABLIST = "tablist",
- TABPANEL = "tabpanel",
- TEXTBOX = "textbox",
- TOOLTIP = "tooltip",
- TREEITEM = "treeitem",
- COMBOBOX = "combobox",
- GRID = "grid",
- GRIDCELL = "gridcell",
- LISTBOX = "listbox",
- LISTITEM = "listitem",
- MENU = "menu",
- MENUBAR = "menubar",
- TREE = "tree",
- TREEGRID = "treegrid",
- WINDOW = "window",
- DIALOG = "dialog",
- ALERTDIALOG = "alertdialog",
-
- -- Landmark roles
- BANNER = "banner",
- COMPLEMENTARY = "complementary",
- CONTENTINFO = "contentinfo",
- FORM = "form",
- MAIN = "main",
- NAVIGATION = "navigation",
- REGION = "region",
- SEARCH = "search",
-
- -- Live region roles
- ALERT = "alert",
- LOG = "log",
- MARQUEE = "marquee",
- STATUS = "status",
- TIMERTIME = "timer",
-
- -- Document structure roles
- ARTICLE = "article",
- BLOCKQUOTEBLOCKQUOTE = "blockquote",
- CAPTION = "caption",
- CODE = "code",
- DEFINITION = "definition",
- DELETED = "deletion",
- DIRECTORY = "directory",
- DIVISION = "division",
- EMphasis = "emphasis",
- HEADING = "heading",
- INSERTED = "insertion",
- LIST = "list",
- MARK = "mark",
- MATH = "math",
- NONE = "none",
- PARAGRAPH = "paragraph",
- PRESENTATION = "presentation",
- SEPARATOR = "separator",
- STRONG = "strong",
- SUBSCRIPT = "subscript",
- SUPERSCRIPT = "superscript",
- TERM = "term",
- TIME = "time",
- VARIABLE = "variable",
- },
-}
-
-return { enums = enums }
diff --git a/libs/flexlove/modules/ErrorHandler.lua b/libs/flexlove/modules/ErrorHandler.lua
deleted file mode 100644
index 2f930619..00000000
--- a/libs/flexlove/modules/ErrorHandler.lua
+++ /dev/null
@@ -1,1042 +0,0 @@
----@class ErrorCodes
----@field categories table
----@field codes table
-local ErrorCodes = {
- categories = {
- VAL = "Validation",
- LAY = "Layout",
- REN = "Render",
- THM = "Theme",
- EVT = "Event",
- RES = "Resource",
- SYS = "System",
- },
- codes = {
- -- Validation Errors (VAL_001 - VAL_099)
- VAL_001 = {
- code = "FLEXLOVE_VAL_001",
- category = "VAL",
- description = "Invalid property type",
- suggestion = "Check the property type matches the expected type (e.g., number, string, table)",
- },
- VAL_002 = {
- code = "FLEXLOVE_VAL_002",
- category = "VAL",
- description = "Property value out of range",
- suggestion = "Ensure the value is within the allowed min/max range",
- },
- VAL_003 = {
- code = "FLEXLOVE_VAL_003",
- category = "VAL",
- description = "Required property missing",
- suggestion = "Provide the required property in your element definition",
- },
- VAL_004 = {
- code = "FLEXLOVE_VAL_004",
- category = "VAL",
- description = "Invalid color format",
- suggestion = "Use valid color format: {r, g, b, a} with values 0-1, hex string, or Color object",
- },
- VAL_005 = {
- code = "FLEXLOVE_VAL_005",
- category = "VAL",
- description = "Invalid unit format",
- suggestion = "Use valid unit format: number (px), '50%', '10vw', '5vh', etc.",
- },
- VAL_006 = {
- code = "FLEXLOVE_VAL_006",
- category = "VAL",
- description = "Invalid calc() expression or calculation error",
- suggestion = "Check calc() syntax and ensure no division by zero. Format: calc('value1 operator value2') with operators: +, -, *, / and units: px, %, vw, vh",
- },
- VAL_007 = {
- code = "FLEXLOVE_VAL_007",
- category = "VAL",
- description = "Invalid enum value",
- suggestion = "Use one of the allowed enum values for this property",
- },
- VAL_008 = {
- code = "FLEXLOVE_VAL_008",
- category = "VAL",
- description = "Invalid text input",
- suggestion = "Ensure text meets validation requirements (length, pattern, allowed characters)",
- },
-
- -- Layout Errors (LAY_001 - LAY_099)
- LAY_001 = {
- code = "FLEXLOVE_LAY_001",
- category = "LAY",
- description = "Invalid flex direction",
- suggestion = "Use 'horizontal' or 'vertical' for flexDirection",
- },
- LAY_002 = {
- code = "FLEXLOVE_LAY_002",
- category = "LAY",
- description = "Circular dependency detected",
- suggestion = "Remove circular references in element hierarchy or layout constraints",
- },
- LAY_003 = {
- code = "FLEXLOVE_LAY_003",
- category = "LAY",
- description = "Invalid dimensions (negative or NaN)",
- suggestion = "Ensure width and height are positive numbers",
- },
- LAY_004 = {
- code = "FLEXLOVE_LAY_004",
- category = "LAY",
- description = "Layout calculation overflow",
- suggestion = "Reduce complexity of layout or increase recursion limit",
- },
- LAY_005 = {
- code = "FLEXLOVE_LAY_005",
- category = "LAY",
- description = "Invalid alignment value",
- suggestion = "Use valid alignment values (flex-start, center, flex-end, etc.)",
- },
- LAY_006 = {
- code = "FLEXLOVE_LAY_006",
- category = "LAY",
- description = "Invalid positioning mode",
- suggestion = "Use 'absolute', 'relative', 'flex', or 'grid' for positioning",
- },
- LAY_007 = {
- code = "FLEXLOVE_LAY_007",
- category = "LAY",
- description = "Grid layout error",
- suggestion = "Check grid template columns/rows and item placement",
- },
- LAY_008 = {
- code = "FLEXLOVE_LAY_008",
- category = "LAY",
- description = "Explicit position will be ignored by flex layout",
- suggestion = "Remove x/y properties (flex layout controls position), OR set positioning='absolute' with left/top/right/bottom properties. Additionally, you can use margin/padding for positional offsets in flex layouts.",
- },
- LAY_009 = {
- code = "FLEXLOVE_LAY_009",
- category = "LAY",
- description = "Flex layout properties ignored with grid positioning",
- suggestion = "Remove flexDirection/justifyContent/alignItems properties, or change positioning to 'flex' or 'relative'",
- },
- LAY_010 = {
- code = "FLEXLOVE_LAY_010",
- category = "LAY",
- description = "Grid layout properties ignored without grid positioning",
- suggestion = "Set positioning='grid' to use grid layout properties, or remove grid properties",
- },
- LAY_011 = {
- code = "FLEXLOVE_LAY_011",
- category = "LAY",
- description = "CSS positioning properties ignored",
- suggestion = "Set positioning='absolute' to use top/bottom/left/right properties",
- },
-
- -- Rendering Errors (REN_001 - REN_099)
- REN_001 = {
- code = "FLEXLOVE_REN_001",
- category = "REN",
- description = "Invalid render state",
- suggestion = "Ensure element is properly initialized before rendering",
- },
- REN_002 = {
- code = "FLEXLOVE_REN_002",
- category = "REN",
- description = "Texture loading failed",
- suggestion = "Check image path and format, ensure file exists",
- },
- REN_003 = {
- code = "FLEXLOVE_REN_003",
- category = "REN",
- description = "Font loading failed",
- suggestion = "Check font path and format, ensure file exists",
- },
- REN_004 = {
- code = "FLEXLOVE_REN_004",
- category = "REN",
- description = "Invalid color value",
- suggestion = "Color components must be numbers between 0 and 1",
- },
- REN_005 = {
- code = "FLEXLOVE_REN_005",
- category = "REN",
- description = "Clipping stack overflow",
- suggestion = "Reduce nesting depth or check for missing scissor pops",
- },
- REN_006 = {
- code = "FLEXLOVE_REN_006",
- category = "REN",
- description = "Shader compilation failed",
- suggestion = "Check shader code for syntax errors",
- },
- REN_007 = {
- code = "FLEXLOVE_REN_007",
- category = "REN",
- description = "Invalid nine-patch configuration",
- suggestion = "Check nine-patch slice values and image dimensions",
- },
-
- -- Theme Errors (THM_001 - THM_099)
- THM_001 = {
- code = "FLEXLOVE_THM_001",
- category = "THM",
- description = "Theme file not found",
- suggestion = "Check theme file path and ensure file exists",
- },
- THM_002 = {
- code = "FLEXLOVE_THM_002",
- category = "THM",
- description = "Invalid theme structure",
- suggestion = "Theme must return a table with 'name' and component styles",
- },
- THM_003 = {
- code = "FLEXLOVE_THM_003",
- category = "THM",
- description = "Required theme property missing",
- suggestion = "Ensure theme has required properties (name, base styles, etc.)",
- },
- THM_004 = {
- code = "FLEXLOVE_THM_004",
- category = "THM",
- description = "Invalid component style",
- suggestion = "Component styles must be tables with valid properties",
- },
- THM_005 = {
- code = "FLEXLOVE_THM_005",
- category = "THM",
- description = "Theme loading failed",
- suggestion = "Check theme file for Lua syntax errors",
- },
- THM_006 = {
- code = "FLEXLOVE_THM_006",
- category = "THM",
- description = "Invalid theme color",
- suggestion = "Theme colors must be valid color values (hex, rgba, Color object)",
- },
- THM_007 = {
- code = "FLEXLOVE_THM_007",
- category = "THM",
- description = "themeStateLock has no effect without a valid theme component",
- suggestion = "Ensure themeComponent is set and valid when using themeStateLock",
- },
- THM_008 = {
- code = "FLEXLOVE_THM_008",
- category = "THM",
- description = "Theme component has no state variants",
- suggestion = "themeStateLock has no effect on components without state variants",
- },
- THM_009 = {
- code = "FLEXLOVE_THM_009",
- category = "THM",
- description = "Requested theme state does not exist",
- suggestion = "Use one of the available theme states or set themeStateLock to false",
- },
- THM_010 = {
- code = "FLEXLOVE_THM_010",
- category = "THM",
- description = "Invalid themeStateLock type",
- suggestion = "themeStateLock must be boolean or string (state name)",
- },
-
- -- Event Errors (EVT_001 - EVT_099)
- EVT_001 = {
- code = "FLEXLOVE_EVT_001",
- category = "EVT",
- description = "Invalid event type",
- suggestion = "Use valid event types (mousepressed, textinput, etc.)",
- },
- EVT_002 = {
- code = "FLEXLOVE_EVT_002",
- category = "EVT",
- description = "Event handler error",
- suggestion = "Check event handler function for errors",
- },
- EVT_003 = {
- code = "FLEXLOVE_EVT_003",
- category = "EVT",
- description = "Event propagation error",
- suggestion = "Check event bubbling/capturing logic",
- },
- EVT_004 = {
- code = "FLEXLOVE_EVT_004",
- category = "EVT",
- description = "Invalid event target",
- suggestion = "Ensure event target element exists and is valid",
- },
- EVT_005 = {
- code = "FLEXLOVE_EVT_005",
- category = "EVT",
- description = "Event handler not a function",
- suggestion = "Event handlers must be functions",
- },
-
- -- Resource Errors (RES_001 - RES_099)
- RES_001 = {
- code = "FLEXLOVE_RES_001",
- category = "RES",
- description = "File not found",
- suggestion = "Check file path and ensure file exists in the filesystem",
- },
- RES_002 = {
- code = "FLEXLOVE_RES_002",
- category = "RES",
- description = "Permission denied",
- suggestion = "Check file permissions and access rights",
- },
- RES_003 = {
- code = "FLEXLOVE_RES_003",
- category = "RES",
- description = "Invalid file format",
- suggestion = "Ensure file format is supported (png, jpg, ttf, etc.)",
- },
- RES_004 = {
- code = "FLEXLOVE_RES_004",
- category = "RES",
- description = "Resource loading failed",
- suggestion = "Check file integrity and format compatibility",
- },
- RES_005 = {
- code = "FLEXLOVE_RES_005",
- category = "RES",
- description = "Image cache error",
- suggestion = "Clear image cache or check memory availability",
- },
-
- -- System Errors (SYS_001 - SYS_099)
- SYS_001 = {
- code = "FLEXLOVE_SYS_001",
- category = "SYS",
- description = "Memory allocation failed",
- suggestion = "Reduce memory usage or check available memory",
- },
- SYS_002 = {
- code = "FLEXLOVE_SYS_002",
- category = "SYS",
- description = "Stack overflow",
- suggestion = "Reduce recursion depth or check for infinite loops",
- },
- SYS_003 = {
- code = "FLEXLOVE_SYS_003",
- category = "SYS",
- description = "Invalid state",
- suggestion = "Check initialization order and state management",
- },
- SYS_004 = {
- code = "FLEXLOVE_SYS_004",
- category = "SYS",
- description = "Module initialization failed",
- suggestion = "Check module dependencies and initialization order",
- },
-
- -- Performance Warnings (PERF_001 - PERF_099)
- PERF_001 = {
- code = "FLEXLOVE_PERF_001",
- category = "PERF",
- description = "Performance threshold exceeded",
- suggestion = "Operation took longer than recommended. Monitor for patterns.",
- },
- PERF_002 = {
- code = "FLEXLOVE_PERF_002",
- category = "PERF",
- description = "Critical performance threshold exceeded",
- suggestion = "Operation is causing frame drops. Consider optimizing or reducing frequency.",
- },
- PERF_003 = {
- code = "FLEXLOVE_PERF_003",
- category = "PERF",
- description = "Large blur area in immediate mode",
- suggestion = "Consider using retained mode for this component to avoid recreating blur effects every frame.",
- },
-
- -- Memory Warnings (MEM_001 - MEM_099)
- MEM_001 = {
- code = "FLEXLOVE_MEM_001",
- category = "MEM",
- description = "Memory leak detected",
- suggestion = "Table is growing consistently. Review cache eviction policies and ensure objects are properly released.",
- },
-
- -- State Management Warnings (STATE_001 - STATE_099)
- STATE_001 = {
- code = "FLEXLOVE_STATE_001",
- category = "STATE",
- description = "CallSite counters accumulating",
- suggestion = "This indicates incrementFrame() may not be called properly. Check immediate mode frame management.",
- },
-
- -- Animation Errors (ANIM_001 - ANIM_099)
- ANIM_001 = {
- code = "FLEXLOVE_ANIM_001",
- category = "VAL",
- description = "Invalid animation configuration",
- suggestion = "Animation.new() requires a table argument with duration, start, and final properties",
- },
- ANIM_002 = {
- code = "FLEXLOVE_ANIM_002",
- category = "VAL",
- description = "Invalid animation duration",
- suggestion = "Animation duration must be a positive number in seconds",
- },
- ANIM_003 = {
- code = "FLEXLOVE_ANIM_003",
- category = "VAL",
- description = "Invalid animation target",
- suggestion = "Animation can only be applied to table elements",
- },
- ANIM_004 = {
- code = "FLEXLOVE_ANIM_004",
- category = "VAL",
- description = "Invalid animation chain",
- suggestion = "chain() requires an Animation object or function",
- },
- ANIM_005 = {
- code = "FLEXLOVE_ANIM_005",
- category = "VAL",
- description = "Invalid animation delay",
- suggestion = "delay() requires a non-negative number in seconds",
- },
- ANIM_006 = {
- code = "FLEXLOVE_ANIM_006",
- category = "VAL",
- description = "Invalid repeat count",
- suggestion = "repeatCount() requires a non-negative number",
- },
- ANIM_007 = {
- code = "FLEXLOVE_ANIM_007",
- category = "VAL",
- description = "Invalid keyframes configuration",
- suggestion = "Animation.keyframes() requires a table with duration and keyframes array",
- },
- ANIM_008 = {
- code = "FLEXLOVE_ANIM_008",
- category = "VAL",
- description = "Insufficient keyframes",
- suggestion = "Keyframe animations require at least 2 keyframes",
- },
- ANIM_009 = {
- code = "FLEXLOVE_ANIM_009",
- category = "VAL",
- description = "Invalid animation group configuration",
- suggestion = "AnimationGroup.new() requires a table with animations array",
- },
- ANIM_010 = {
- code = "FLEXLOVE_ANIM_010",
- category = "VAL",
- description = "Empty animation group",
- suggestion = "AnimationGroup requires at least one animation",
- },
- ANIM_011 = {
- code = "FLEXLOVE_ANIM_011",
- category = "VAL",
- description = "Invalid animation group mode",
- suggestion = "AnimationGroup mode must be 'parallel' or 'sequence'",
- },
-
- -- Blur Errors (BLUR_001 - BLUR_099)
- BLUR_001 = {
- code = "FLEXLOVE_BLUR_001",
- category = "VAL",
- description = "Missing draw function",
- suggestion = "applyToRegion requires a draw function to render the content to be blurred",
- },
- BLUR_002 = {
- code = "FLEXLOVE_BLUR_002",
- category = "VAL",
- description = "Missing backdrop canvas",
- suggestion = "applyBackdrop requires a backdrop canvas parameter",
- },
-
- -- FlexLove Core Errors (CORE_001 - CORE_099)
- CORE_001 = {
- code = "FLEXLOVE_CORE_001",
- category = "VAL",
- description = "Invalid callback function",
- suggestion = "deferCallback expects a function argument",
- },
- CORE_002 = {
- code = "FLEXLOVE_CORE_002",
- category = "SYS",
- description = "Deferred callback execution failed",
- suggestion = "Check the callback function for errors. Error details included in message.",
- },
- CORE_003 = {
- code = "FLEXLOVE_CORE_003",
- category = "VAL",
- description = "Invalid garbage collection strategy",
- suggestion = "GC strategy must be one of: 'default', 'aggressive', 'conservative'",
- },
- CORE_004 = {
- code = "FLEXLOVE_CORE_004",
- category = "SYS",
- description = "Deferred method retry limit exceeded",
- suggestion = "A deferred method has been retried too many times without succeeding. Check that preconditions are eventually met.",
- },
- CORE_005 = {
- code = "FLEXLOVE_CORE_005",
- category = "VAL",
- description = "Invalid deferred method",
- suggestion = "The method name provided to _deferMethod does not exist on the element.",
- },
-
- -- Element Errors (ELEM_001 - ELEM_099)
- ELEM_001 = {
- code = "FLEXLOVE_ELEM_001",
- category = "VAL",
- description = "Invalid text size",
- suggestion = "textSize must be greater than 0",
- },
- ELEM_002 = {
- code = "FLEXLOVE_ELEM_002",
- category = "VAL",
- description = "Invalid text size unit",
- suggestion = "textSize unit must be one of: px, %, vw, vh, or presets: xs, sm, md, lg, xl, xxl, 2xl, 3xl, 4xl",
- },
- ELEM_003 = {
- code = "FLEXLOVE_ELEM_003",
- category = "VAL",
- description = "Invalid transition configuration",
- suggestion = "setTransition() requires a table with transition properties",
- },
- ELEM_004 = {
- code = "FLEXLOVE_ELEM_004",
- category = "VAL",
- description = "Invalid transition duration",
- suggestion = "Transition duration must be a non-negative number in seconds",
- },
- ELEM_005 = {
- code = "FLEXLOVE_ELEM_005",
- category = "VAL",
- description = "Invalid transition group",
- suggestion = "setTransitionGroup() requires an array of property names",
- },
- ELEM_006 = {
- code = "FLEXLOVE_ELEM_006",
- category = "VAL",
- description = "Incompatible element configuration",
- suggestion = "passwordMode and multiline cannot be used together. Multiline will be disabled.",
- },
- ELEM_007 = {
- code = "FLEXLOVE_ELEM_007",
- category = "VAL",
- description = "Invalid select frame configuration",
- suggestion = "Pass a fully instantiated Element as selectParent.selectFrame. Create it unattached so the owning select can adopt it safely.",
- },
- ELEM_008 = {
- code = "FLEXLOVE_ELEM_008",
- category = "VAL",
- description = "Select frame was already parented before adoption",
- suggestion = "Create the selectFrame without a parent, or explicitly accept that the select will reparent it during adoption.",
- },
- ELEM_009 = {
- code = "FLEXLOVE_ELEM_009",
- category = "VAL",
- description = "Managed select frame was reparented unexpectedly",
- suggestion = "Avoid moving a managed selectFrame outside its owning select after adoption. Let the select own the frame lifecycle.",
- },
- ELEM_010 = {
- code = "FLEXLOVE_ELEM_010",
- category = "VAL",
- description = "Invalid display property value",
- suggestion = "The display property accepts only boolean values (true/false). Pass `true` to show the element or `false` to hide it from layout, rendering, and hit testing.",
- },
-
- -- Module Loader Warnings (MOD_001 - MOD_099)
- MOD_001 = {
- code = "FLEXLOVE_MOD_001",
- category = "RES",
- description = "Optional module not found",
- suggestion = "Using stub implementation for optional module. This is expected if the module is not required.",
- },
-
- -- Utility Errors (UTIL_001 - UTIL_099)
- UTIL_001 = {
- code = "FLEXLOVE_UTIL_001",
- category = "VAL",
- description = "Text truncation warning",
- suggestion = "Text was truncated to fit within the maximum allowed length",
- },
-
- -- Image/Rendering Errors (IMG_001 - IMG_099)
- IMG_001 = {
- code = "FLEXLOVE_IMG_001",
- category = "REN",
- description = "Stencil buffer not available",
- suggestion = "Cannot apply corner radius to image without stencil buffer support. Check graphics capabilities.",
- },
-
- -- Navigation Errors (NAV_001 - NAV_099)
- NAV_001 = {
- code = "FLEXLOVE_NAV_001",
- category = "EVT",
- description = "Element focus callback error",
- suggestion = "Check the onFocus callback function for errors. Error details included in message.",
- },
- NAV_002 = {
- code = "FLEXLOVE_NAV_002",
- category = "EVT",
- description = "Element activation callback error",
- suggestion = "Check the onEvent callback function for errors. Error details included in message.",
- },
- NAV_003 = {
- code = "FLEXLOVE_NAV_003",
- category = "EVT",
- description = "Element dismiss callback error",
- suggestion = "Check the onDismiss callback function for errors. Error details included in message.",
- },
- },
-}
-
---- Get error information by code
---- @param code string Error code (e.g., "VAL_001" or "FLEXLOVE_VAL_001")
---- @return table? errorInfo Error information or nil if not found
-function ErrorCodes.get(code)
- -- Handle both short and full format
- local shortCode = code:gsub("^FLEXLOVE_", "")
- return ErrorCodes.codes[shortCode]
-end
-
---- Get human-readable description for error code
---- @param code string Error code
---- @return string description Error description
-function ErrorCodes.describe(code)
- local info = ErrorCodes.get(code)
- if info then
- return info.description
- end
- return "Unknown error code: " .. code
-end
-
---- Search error codes by keyword
---- @param keyword string Keyword to search for
---- @return table codes Matching error codes
-function ErrorCodes.search(keyword)
- keyword = keyword:lower()
- local result = {}
- for code, info in pairs(ErrorCodes.codes) do
- local searchText = (code .. " " .. info.description .. " " .. info.suggestion):lower()
- if searchText:find(keyword, 1, true) then
- table.insert(result, {
- code = code,
- fullCode = info.code,
- description = info.description,
- suggestion = info.suggestion,
- category = ErrorCodes.categories[info.category],
- })
- end
- end
- return result
-end
-
---- Format error message with code
---- @param code string Error code
---- @param message string Error message
---- @return string formattedMessage Formatted error message with code
-function ErrorCodes.formatMessage(code, message)
- local info = ErrorCodes.get(code)
- if info then
- return string.format("[%s] %s", info.code, message)
- end
- return message
-end
-
---- Validate that all error codes are unique and properly formatted
---- @return boolean, string? Returns true if valid, or false with error message
-function ErrorCodes.validate()
- local seen = {}
- local fullCodes = {}
-
- for code, info in pairs(ErrorCodes.codes) do
- -- Check for duplicates
- if seen[code] then
- return false, "Duplicate error code: " .. code
- end
- seen[code] = true
-
- if fullCodes[info.code] then
- return false, "Duplicate full error code: " .. info.code
- end
- fullCodes[info.code] = true
-
- -- Check format
- if not code:match("^[A-Z]+_[0-9]+$") then
- return false, "Invalid code format: " .. code .. " (expected CATEGORY_NUMBER)"
- end
-
- -- Check full code format
- local expectedFullCode = "FLEXLOVE_" .. code
- if info.code ~= expectedFullCode then
- return false, "Mismatched full code for " .. code .. ": expected " .. expectedFullCode .. ", got " .. info.code
- end
-
- -- Check required fields
- if not info.description or info.description == "" then
- return false, "Missing description for " .. code
- end
- if not info.suggestion or info.suggestion == "" then
- return false, "Missing suggestion for " .. code
- end
- if not info.category or info.category == "" then
- return false, "Missing category for " .. code
- end
- end
-
- return true, nil
-end
-
----@enum LOG_LEVEL
-local LOG_LEVEL = {
- CRITICAL = 1,
- ERROR = 2,
- WARNING = 3,
- INFO = 4,
- DEBUG = 5,
-}
-
----@enum LOG_TARGET
-local LOG_TARGET = {
- CONSOLE = "console",
- FILE = "file",
- BOTH = "both",
- NONE = "none",
-}
-
----@class ErrorHandler
----@field errorCodes ErrorCodes
----@field includeStackTrace boolean -- Default: false
----@field logLevel LOG_LEVEL --Default: LOG_LEVEL.WARNING
----@field logTarget "console" | "file" | "both"
----@field logFile string
----@field maxLogSize number in bytes
----@field maxLogFiles number files to rotate
----@field enableRotation boolean see maxLogFiles
----@field _currentLogSize number private
----@field _logFileHandle file* private
-local ErrorHandler = {
- errorCodes = ErrorCodes,
-}
-ErrorHandler.__index = ErrorHandler
-
----@type ErrorHandler|nil
-local instance = nil
-
----@param config { includeStackTrace?: boolean, logLevel?: LOG_LEVEL, logTarget?: "console" | "file" | "both", logFile?: string, maxLogSize?: number, maxLogFiles?: number, enableRotation?: boolean }|nil
----@return ErrorHandler
-function ErrorHandler.init(config)
- if instance == nil then
- local self = setmetatable({}, ErrorHandler)
- self.includeStackTrace = config and config.includeStackTrace or false
- self.logLevel = config and config.logLevel or LOG_LEVEL.WARNING
- self.logTarget = config and config.logTarget or LOG_TARGET.CONSOLE
- self.logFile = config and config.logFile or "flexlove-errors.log"
- self.maxLogSize = config and config.maxLogSize or 10 * 1024 * 1024
- self.maxLogFiles = config and config.maxLogFiles or 5
- self.enableRotation = config and config.enableRotation or true
- self._currentLogSize = 0
- self._logFileHandle = nil
- instance = self
- end
- return instance
-end
-
---- Get the singleton instance (lazily initializes if needed)
----@return ErrorHandler
-function ErrorHandler.getInstance()
- if instance == nil then
- ErrorHandler.init()
- end
- return instance
-end
-
---- Get current timestamp with milliseconds
----@return string|osdate Formatted timestamp
-function ErrorHandler:_getTimestamp()
- local time = os.time()
- local date = os.date("%Y-%m-%d %H:%M:%S", time)
- -- Note: Lua doesn't have millisecond precision by default, so we approximate
- return date
-end
-
---- Rotate log file if needed
-function ErrorHandler:_rotateLogIfNeeded()
- if not self.enableRotation then
- return
- end
- if self._currentLogSize < self.maxLogSize then
- return
- end
-
- -- Close current log
- if self._logFileHandle then
- self._logFileHandle:close()
- self._logFileHandle = nil
- end
-
- -- Rotate existing logs
- for i = self.maxLogFiles - 1, 1, -1 do
- local oldName = self.logFile .. "." .. i
- local newName = self.logFile .. "." .. (i + 1)
- os.rename(oldName, newName) -- Will fail silently if file doesn't exist
- end
-
- -- Move current log to .1
- os.rename(self.logFile, self.logFile .. ".1")
-
- -- Create new log file
- self._logFileHandle = io.open(self.logFile, "a")
- self._currentLogSize = 0
-end
-
---- Escape string for JSON
----@param str string String to escape
----@return string Escaped string
-function ErrorHandler:_escapeJson(str)
- str = tostring(str)
- str = str:gsub("\\", "\\\\")
- str = str:gsub('"', '\\"')
- str = str:gsub("\n", "\\n")
- str = str:gsub("\r", "\\r")
- str = str:gsub("\t", "\\t")
- return str
-end
-
---- Format details as JSON object
----@param details table|nil Details object
----@return string JSON string
-function ErrorHandler:_formatDetailsJson(details)
- if not details or type(details) ~= "table" then
- return "{}"
- end
-
- local parts = {}
- for key, value in pairs(details) do
- local jsonKey = self:_escapeJson(tostring(key))
- local jsonValue = self:_escapeJson(tostring(value))
- table.insert(parts, string.format('"%s":"%s"', jsonKey, jsonValue))
- end
-
- return "{" .. table.concat(parts, ",") .. "}"
-end
-
---- Format details object as readable key-value pairs
----@param details table|nil Details object
----@return string Formatted details
-function ErrorHandler:_formatDetails(details)
- if not details or type(details) ~= "table" then
- return ""
- end
-
- local lines = {}
- for key, value in pairs(details) do
- local formattedKey = tostring(key):gsub("^%l", string.upper)
- local formattedValue = tostring(value)
- -- Truncate very long values
- if #formattedValue > 100 then
- formattedValue = formattedValue:sub(1, 97) .. "..."
- end
- table.insert(lines, string.format(" %s: %s", formattedKey, formattedValue))
- end
-
- if #lines > 0 then
- return "\n\nDetails:\n" .. table.concat(lines, "\n")
- end
- return ""
-end
-
---- Extract and format stack trace
----@param level number Stack level to start from
----@return string Formatted stack trace
-function ErrorHandler:_formatStackTrace(level)
- if not self.includeStackTrace then
- return ""
- end
-
- local lines = {}
- local currentLevel = level or 3
-
- while true do
- local info = debug.getinfo(currentLevel, "Sl")
- if not info then
- break
- end
-
- -- Skip internal Lua files
- if info.source:match("^@") and not info.source:match("loveStub") then
- local source = info.source:sub(2) -- Remove @ prefix
- local location = string.format("%s:%d", source, info.currentline)
- table.insert(lines, " " .. location)
- end
-
- currentLevel = currentLevel + 1
- if currentLevel > level + 10 then
- break
- end -- Limit depth
- end
-
- if #lines > 0 then
- return "\n\nStack trace:\n" .. table.concat(lines, "\n")
- end
- return ""
-end
-
---- Format an error or warning message using error code lookup
----@param module string The module name (e.g., "Element", "Units", "Theme")
----@param level string "Error" or "Warning"
----@param code string Error code (e.g., "VAL_001")
----@param details table|nil Optional details object
----@return string Formatted message
-function ErrorHandler:_formatMessage(module, level, code, details)
- local codeInfo = ErrorCodes.get(code)
-
- if not codeInfo then
- return string.format("[FlexLove - %s] %s: Unknown error code: %s", module, level, code)
- end
-
- -- Build formatted message
- local parts = {}
-
- -- Header: [FlexLove - Module] Level [CODE]: Description
- table.insert(parts, string.format("[FlexLove - %s] %s [%s]: %s", module, level, codeInfo.code, codeInfo.description))
-
- -- Details section
- if details and type(details) == "table" then
- table.insert(parts, self:_formatDetails(details))
- end
-
- -- Suggestion section
- if codeInfo.suggestion and codeInfo.suggestion ~= "" then
- table.insert(parts, string.format("\n\nSuggestion: %s", codeInfo.suggestion))
- end
-
- return table.concat(parts, "")
-end
-
---- Write log entry to file and/or console
----@param level string Log level
----@param levelNum number Log level number
----@param module string Module name
----@param code string|nil Error code
----@param message string Message
----@param details table|nil Details
----@param suggestion string|nil Suggestion
-function ErrorHandler:_writeLog(level, levelNum, module, code, message, details, suggestion)
- -- Check if we should log this level
- if not levelNum or not self.logLevel or levelNum > self.logLevel then
- return
- end
-
- local timestamp = self:_getTimestamp()
- local logEntry
-
- local jsonParts = {
- string.format('"timestamp":"%s"', self:_escapeJson(timestamp)),
- string.format('"level":"%s"', level),
- string.format('"module":"%s"', self:_escapeJson(module)),
- string.format('"message":"%s"', self:_escapeJson(message)),
- }
-
- if code then
- table.insert(jsonParts, string.format('"code":"%s"', self:_escapeJson(code)))
- end
-
- if details then
- table.insert(jsonParts, string.format('"details":%s', self:_formatDetailsJson(details)))
- end
-
- if suggestion then
- table.insert(jsonParts, string.format('"suggestion":"%s"', self:_escapeJson(suggestion)))
- end
-
- logEntry = "{" .. table.concat(jsonParts, ",") .. "}\n"
-
- if self.logTarget == "console" or self.logTarget == "both" then
- io.write(logEntry)
- io.flush()
- end
-
- -- Write to file
- if self.logTarget == "file" or self.logTarget == "both" then
- -- Lazy file opening: open on first write
- if not self._logFileHandle then
- self._logFileHandle = io.open(self.logFile, "a")
- if self._logFileHandle then
- -- Get current file size
- local currentPos = self._logFileHandle:seek("end")
- self._currentLogSize = currentPos or 0
- end
- end
-
- if self._logFileHandle then
- self:_rotateLogIfNeeded()
-
- -- Reopen if rotation closed it
- if not self._logFileHandle then
- self._logFileHandle = io.open(self.logFile, "a")
- end
-
- if self._logFileHandle then
- self._logFileHandle:write(logEntry)
- self._logFileHandle:flush()
- self._currentLogSize = self._currentLogSize + #logEntry
- end
- end
- end
-end
-
---- Throw a critical error (stops execution)
----@param module string The module name
----@param code string Error code (e.g., "VAL_001")
----@param details table|nil Optional details object
-function ErrorHandler:error(module, code, details)
- local formattedMessage = self:_formatMessage(module, "Error", code, details)
-
- local codeInfo = ErrorCodes.get(code)
- local message = codeInfo and codeInfo.description or code
- local suggestion = codeInfo and codeInfo.suggestion or nil
-
- -- Log the error
- self:_writeLog("ERROR", LOG_LEVEL.ERROR, module, code, message, details, suggestion)
-
- if self.includeStackTrace then
- formattedMessage = formattedMessage .. self:_formatStackTrace(3)
- end
-
- error(formattedMessage, 2)
-end
-
---- Print a warning (non-critical, continues execution)
----@param module string The module name
----@param code string Warning code (e.g., "VAL_001")
----@param details table|nil Optional details object
-function ErrorHandler:warn(module, code, details)
- local codeInfo = ErrorCodes.get(code)
- local message = codeInfo and codeInfo.description or code
- local suggestion = codeInfo and codeInfo.suggestion or nil
-
- -- Log the warning
- self:_writeLog("WARNING", LOG_LEVEL.WARNING, module, code, message, details, suggestion)
-end
-
---- Validate that a value is not nil
----@param module string The module name
----@param value any The value to check
----@param paramName string The parameter name
----@return boolean True if valid
-function ErrorHandler:assertNotNil(module, value, paramName)
- if value == nil then
- self:error(module, "VAL_003", "Required parameter missing", {
- parameter = paramName,
- })
- return false
- end
- return true
-end
-
---- Validate that a value is of the expected type
----@param module string The module name
---- Warn if a value is deprecated
----@param module string The module name
----@param oldName string The deprecated name
----@param newName string The new name to use
-function ErrorHandler:warnDeprecated(module, oldName, newName)
- self:warn(module, string.format("'%s' is deprecated. Use '%s' instead", oldName, newName))
-end
-
-return ErrorHandler
diff --git a/libs/flexlove/modules/EventHandler.lua b/libs/flexlove/modules/EventHandler.lua
deleted file mode 100644
index 2631fc47..00000000
--- a/libs/flexlove/modules/EventHandler.lua
+++ /dev/null
@@ -1,843 +0,0 @@
----@class EventHandler
----@field onEvent fun(element:Element, event:InputEvent)?
----@field onEventDeferred boolean?
----@field onTouchEvent fun(element:Element, touchEvent:InputEvent)? -- Touch-specific callback
----@field onTouchEventDeferred boolean? -- Whether onTouchEvent is deferred
----@field onGesture fun(element:Element, gesture:table)? -- Gesture callback
----@field onGestureDeferred boolean? -- Whether onGesture is deferred
----@field touchEnabled boolean -- Whether touch events are processed (default: true)
----@field multiTouchEnabled boolean -- Whether multi-touch is supported (default: false)
----@field _pressed table
----@field _lastClickTime number?
----@field _lastClickButton number?
----@field _clickCount number
----@field _dragStartX table
----@field _dragStartY table
----@field _lastMouseX table
----@field _lastMouseY table
----@field _touches table -- Multi-touch state per touch ID
----@field _touchStartPositions table -- Touch start positions
----@field _lastTouchPositions table -- Last touch positions for delta
----@field _touchHistory table -- Touch position history for gestures (last 5)
----@field _hovered boolean
----@field _scrollbarPressHandled boolean
----@field _InputEvent table
----@field _utils table
----@field _Performance Performance? Performance module dependency
----@field _ErrorHandler ErrorHandler
-local EventHandler = {}
-EventHandler.__index = EventHandler
-
---- Initialize module with shared dependencies
----@param deps table Dependencies {Performance, ErrorHandler, InputEvent, Context, utils}
-function EventHandler.init(deps)
- EventHandler._Performance = deps.Performance
- EventHandler._ErrorHandler = deps.ErrorHandler
- EventHandler._InputEvent = deps.InputEvent
- EventHandler._utils = deps.utils
- EventHandler._Context = deps.Context
-end
-
----@param config table Configuration options
----@return EventHandler
-function EventHandler.new(config)
- config = config or {}
- local self = setmetatable({}, EventHandler)
-
- self.onEvent = config.onEvent
- self.onEventDeferred = config.onEventDeferred
- self.onTouchEvent = config.onTouchEvent
- self.onTouchEventDeferred = config.onTouchEventDeferred or false
- self.onGesture = config.onGesture
- self.onGestureDeferred = config.onGestureDeferred or false
- self.touchEnabled = config.touchEnabled ~= false -- Default true
- self.multiTouchEnabled = config.multiTouchEnabled or false -- Default false
-
- self._pressed = config._pressed or {}
-
- self._lastClickTime = config._lastClickTime
- self._lastClickButton = config._lastClickButton
- self._clickCount = config._clickCount or 0
-
- -- FocusIndicator reference (set after initialization)
- self._FocusIndicator = nil
-
- self._dragStartX = config._dragStartX or {}
- self._dragStartY = config._dragStartY or {}
- self._lastMouseX = config._lastMouseX or {}
- self._lastMouseY = config._lastMouseY or {}
-
- -- Multi-touch tracking
- self._touches = config._touches or {}
- self._touchStartPositions = config._touchStartPositions or {}
- self._lastTouchPositions = config._lastTouchPositions or {}
- self._touchHistory = config._touchHistory or {}
-
- self._hovered = config._hovered or false
-
- self._scrollbarPressHandled = false
-
- return self
-end
-
---- Get state for persistence (for immediate mode)
----@return table State data
-function EventHandler:getState()
- return {
- _pressed = self._pressed,
- _lastClickTime = self._lastClickTime,
- _lastClickButton = self._lastClickButton,
- _clickCount = self._clickCount,
- _dragStartX = self._dragStartX,
- _dragStartY = self._dragStartY,
- _lastMouseX = self._lastMouseX,
- _lastMouseY = self._lastMouseY,
- _touches = self._touches,
- _touchStartPositions = self._touchStartPositions,
- _lastTouchPositions = self._lastTouchPositions,
- _touchHistory = self._touchHistory,
- _hovered = self._hovered,
- }
-end
-
---- Restore state from persistence (for immediate mode)
----@param state table State data
-function EventHandler:setState(state)
- if not state then
- return
- end
-
- self._pressed = state._pressed or {}
- self._lastClickTime = state._lastClickTime
- self._lastClickButton = state._lastClickButton
- self._clickCount = state._clickCount or 0
- self._dragStartX = state._dragStartX or {}
- self._dragStartY = state._dragStartY or {}
- self._lastMouseX = state._lastMouseX or {}
- self._lastMouseY = state._lastMouseY or {}
- self._touches = state._touches or {}
- self._touchStartPositions = state._touchStartPositions or {}
- self._lastTouchPositions = state._lastTouchPositions or {}
- self._touchHistory = state._touchHistory or {}
- self._hovered = state._hovered or false
-end
-
---- Process mouse button events in the update cycle
----@param element Element The parent element
----@param mx number Mouse X position
----@param my number Mouse Y position
----@param isHovering boolean Whether mouse is over element
----@param isActiveElement boolean Whether this is the top element at mouse position
-function EventHandler:processMouseEvents(element, mx, my, isHovering, isActiveElement)
- -- Start performance timing
- -- Performance accessed via EventHandler._Performance
- if EventHandler._Performance and EventHandler._Performance.enabled then
- EventHandler._Performance:startTimer("event_mouse")
- end
-
- -- Check if currently dragging (allows drag continuation even if occluded)
- local isDragging = false
- for _, button in ipairs({ 1, 2, 3 }) do
- if self._pressed[button] and love.mouse.isDown(button) then
- isDragging = true
- break
- end
- end
-
- -- Check if any button is currently pressed (tracked state)
- local hasTrackedPress = false
- for _, button in ipairs({ 1, 2, 3 }) do
- if self._pressed[button] then
- hasTrackedPress = true
- break
- end
- end
-
- -- Can only process events if we have handler, element is enabled, and is active or dragging or has tracked press
- -- Read onEvent from element (source of truth), fallback to handler cache for backwards compat
- local canProcessEvents = (
- element.onEvent
- or self.onEvent
- or element.editable
- or element._selectState
- or element.selectOption
- )
- and element.visibility ~= "hidden"
- and not element.disabled
- and (isActiveElement or isDragging or hasTrackedPress)
-
- if not canProcessEvents then
- -- If not hovering and no buttons are physically pressed, reset all pressed states
- -- This ensures the pressed state is cleared when mouse leaves without button held
- if not isHovering and not isDragging then
- for _, button in ipairs({ 1, 2, 3 }) do
- if self._pressed[button] and not love.mouse.isDown(button) then
- self._pressed[button] = false
- self._dragStartX[button] = nil
- self._dragStartY[button] = nil
- end
- end
- end
-
- -- Track hover state changes even when events can't be processed
- -- Fire synthetic unhover when element becomes disabled while hovered
- if element.disabled and self._hovered then
- self._hovered = false
- if element.onEvent or self.onEvent then
- local modifiers = EventHandler._utils.getModifiers()
- local unhoverEvent = EventHandler._InputEvent.new({
- type = "unhover",
- button = 0,
- x = mx,
- y = my,
- modifiers = modifiers,
- clickCount = 0,
- })
- self:_invokeCallback(element, unhoverEvent)
- end
- elseif self._hovered and not isHovering then
- self._hovered = false
- if element.onEvent or self.onEvent then
- local modifiers = EventHandler._utils.getModifiers()
- local unhoverEvent = EventHandler._InputEvent.new({
- type = "unhover",
- button = 0,
- x = mx,
- y = my,
- modifiers = modifiers,
- clickCount = 0,
- })
- self:_invokeCallback(element, unhoverEvent)
- end
- end
-
- if EventHandler._Performance and EventHandler._Performance.enabled then
- EventHandler._Performance:stopTimer("event_mouse")
- end
- return
- end
-
- -- Track hover state changes and fire hover/unhover events BEFORE button processing
- -- This ensures hover fires before press when mouse first enters element
- local wasHovered = self._hovered
- local isHoveringAndActive = isHovering and isActiveElement
-
- if isHoveringAndActive and not wasHovered then
- -- Just started hovering - fire hover event
- self._hovered = true
- local modifiers = EventHandler._utils.getModifiers()
- local hoverEvent = EventHandler._InputEvent.new({
- type = "hover",
- button = 0,
- x = mx,
- y = my,
- modifiers = modifiers,
- clickCount = 0,
- })
- self:_invokeCallback(element, hoverEvent)
- elseif not isHoveringAndActive and wasHovered then
- -- Just stopped hovering - fire unhover event
- self._hovered = false
- local modifiers = EventHandler._utils.getModifiers()
- local unhoverEvent = EventHandler._InputEvent.new({
- type = "unhover",
- button = 0,
- x = mx,
- y = my,
- modifiers = modifiers,
- clickCount = 0,
- })
- self:_invokeCallback(element, unhoverEvent)
- end
-
- -- Process all three mouse buttons
- local buttons = { 1, 2, 3 } -- left, right, middle
-
- for _, button in ipairs(buttons) do
- -- Check if this button was tracked as pressed
- local wasPressed = self._pressed[button]
- local isPhysicallyPressed = love.mouse.isDown(button)
-
- if isHovering or isDragging or wasPressed then
- if isPhysicallyPressed then
- -- Button is pressed down
- if not wasPressed then
- -- Just pressed - fire press event (only if hovering)
- if isHovering then
- self:_handleMousePress(element, mx, my, button)
- end
- else
- -- Button is still pressed - check for drag
- self:_handleMouseDrag(element, mx, my, button, isHovering)
- end
- elseif wasPressed then
- -- Button was just released
- -- Only fire click and release events if mouse is still hovering AND element is active
- -- (not occluded by another element)
- if isHovering and isActiveElement then
- self:_handleMouseRelease(element, mx, my, button)
- else
- -- Mouse left before release OR element is occluded - just clear the pressed state without firing events
- self._pressed[button] = false
- self._dragStartX[button] = nil
- self._dragStartY[button] = nil
- end
- end
- end
- end
-
- -- After processing events, reset pressed states for buttons that are no longer held
- -- This handles the case where mouse leaves while button is held, then released
- if not isHovering and not isDragging then
- for _, button in ipairs({ 1, 2, 3 }) do
- if self._pressed[button] and not love.mouse.isDown(button) then
- self._pressed[button] = false
- self._dragStartX[button] = nil
- self._dragStartY[button] = nil
- end
- end
- end
-
- -- Stop performance timing
- if EventHandler._Performance and EventHandler._Performance.enabled then
- EventHandler._Performance:stopTimer("event_mouse")
- end
-end
-
---- Handle mouse button press
----@param element Element The parent element
----@param mx number Mouse X position
----@param my number Mouse Y position
----@param button number Mouse button (1=left, 2=right, 3=middle)
-function EventHandler:_handleMousePress(element, mx, my, button)
- -- Check if press is on scrollbar first (skip if already handled)
- if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then
- if element:_handleScrollbarPress(mx, my, button) then
- -- Scrollbar consumed the event, mark as pressed to prevent onEvent
- self._pressed[button] = true
- self._scrollbarPressHandled = true
- return
- end
- end
-
- -- Fire press event
- local modifiers = EventHandler._utils.getModifiers()
- local pressEvent = EventHandler._InputEvent.new({
- type = "press",
- button = button,
- x = mx,
- y = my,
- modifiers = modifiers,
- clickCount = 1,
- })
- self:_invokeCallback(element, pressEvent)
-
- self._pressed[button] = true
-
- -- On left click, set keyboard focus to any focusable element (not just editable).
- -- Clear the focus indicator since mouse navigation doesn't use it.
- local isFocusable
- if type(element.isFocusable) == "function" then
- isFocusable = element:isFocusable()
- else
- isFocusable = (element.editable == true)
- or (type(element.onEvent) == "function")
- or element._selectState ~= nil
- or element.selectOption ~= nil
- end
-
- if button == 1 and EventHandler._Context and isFocusable then
- EventHandler._Context.setFocused(element)
- -- Hide focus indicator - it's only for keyboard navigation
- if EventHandler._FocusIndicator then
- EventHandler._FocusIndicator.setFocused(nil)
- end
- end
- -- Set mouse down position for text selection on left click
- if button == 1 and element._textEditor then
- element._mouseDownPosition = element._textEditor:mouseToTextPosition(element, mx, my)
- element._textDragOccurred = false -- Reset drag flag on press
- end
-
- -- Record drag start position per button
- self._dragStartX[button] = mx
- self._dragStartY[button] = my
- self._lastMouseX[button] = mx
- self._lastMouseY[button] = my
-end
-
---- Handle mouse drag (while button is pressed and mouse moves)
----@param element Element The parent element
----@param mx number Mouse X position
----@param my number Mouse Y position
----@param button number Mouse button
----@param isHovering boolean Whether mouse is over element
-function EventHandler:_handleMouseDrag(element, mx, my, button, isHovering)
- local lastX = self._lastMouseX[button] or mx
- local lastY = self._lastMouseY[button] or my
-
- if lastX ~= mx or lastY ~= my then
- -- Handle scrollbar drag if scrollbar was pressed
- if button == 1 and self._scrollbarPressHandled and element._handleScrollbarDrag then
- element:_handleScrollbarDrag(mx, my)
- self._lastMouseX[button] = mx
- self._lastMouseY[button] = my
- return -- Don't process other drag events while dragging scrollbar
- end
-
- -- Mouse has moved - fire drag event only if still hovering
- if isHovering then
- local modifiers = EventHandler._utils.getModifiers()
- local dx = mx - self._dragStartX[button]
- local dy = my - self._dragStartY[button]
-
- local dragEvent = EventHandler._InputEvent.new({
- type = "drag",
- button = button,
- x = mx,
- y = my,
- dx = dx,
- dy = dy,
- modifiers = modifiers,
- clickCount = 1,
- })
- self:_invokeCallback(element, dragEvent)
- end
-
- -- Handle text selection drag for editable elements
- if button == 1 and element.editable and element._focused and element._handleTextDrag then
- element:_handleTextDrag(mx, my)
- end
-
- -- Update last known position for this button
- self._lastMouseX[button] = mx
- self._lastMouseY[button] = my
- end
-end
-
---- Handle mouse button release
----@param mx number Mouse X position
----@param my number Mouse Y position
----@param button number Mouse button
-function EventHandler:_handleMouseRelease(element, mx, my, button)
- local currentTime = love.timer.getTime()
- local modifiers = EventHandler._utils.getModifiers()
-
- -- Handle scrollbar release if scrollbar was pressed
- if button == 1 and self._scrollbarPressHandled and element._handleScrollbarRelease then
- element:_handleScrollbarRelease(button)
- self._scrollbarPressHandled = false -- Reset flag
- self._pressed[button] = false
- self._dragStartX[button] = nil
- self._dragStartY[button] = nil
- return -- Don't process click events for scrollbar release
- end
-
- -- Determine click count (double-click detection)
- local clickCount
- local doubleClickThreshold = 0.3 -- 300ms for double-click
-
- if
- self._lastClickTime
- and self._lastClickButton == button
- and (currentTime - self._lastClickTime) < doubleClickThreshold
- then
- clickCount = self._clickCount + 1
- else
- clickCount = 1
- end
-
- self._clickCount = clickCount
- self._lastClickTime = currentTime
- self._lastClickButton = button
-
- -- Determine event type based on button
- local eventType = "click"
- if button == 2 then
- eventType = "rightclick"
- elseif button == 3 then
- eventType = "middleclick"
- end
-
- -- Fire click event
- local clickEvent = EventHandler._InputEvent.new({
- type = eventType,
- button = button,
- x = mx,
- y = my,
- modifiers = modifiers,
- clickCount = clickCount,
- })
- self:_invokeCallback(element, clickEvent)
-
- self._pressed[button] = false
-
- -- Clean up drag tracking
- self._dragStartX[button] = nil
- self._dragStartY[button] = nil
-
- -- Clean up text selection drag tracking
- if button == 1 then
- element._mouseDownPosition = nil
- end
-
- -- Focus editable elements on left click
- if button == 1 and element.editable then
- -- Only focus if not already focused (to avoid moving cursor to end)
- local wasFocused = element:isFocused()
- if not wasFocused then
- element:focus()
- end
-
- -- Handle text click for cursor positioning and word selection
- -- Only process click if no text drag occurred (to preserve drag selection)
- if element._handleTextClick and not element._textDragOccurred then
- element:_handleTextClick(mx, my, clickCount)
- end
-
- -- Reset drag flag after release
- element._textDragOccurred = false
- end
-
- -- Fire release event
- local releaseEvent = EventHandler._InputEvent.new({
- type = "release",
- button = button,
- x = mx,
- y = my,
- modifiers = modifiers,
- clickCount = clickCount,
- })
- self:_invokeCallback(element, releaseEvent)
-
- if button == 1 and element._handleSelectRelease then
- element:_handleSelectRelease()
- end
-end
-
---- Process touch events in the update cycle
----@param element Element The parent element
-function EventHandler:processTouchEvents(element)
- -- Start performance timing
- if EventHandler._Performance and EventHandler._Performance.enabled then
- EventHandler._Performance:startTimer("event_touch")
- end
-
- -- Check if element can process events
- local canProcessEvents = (
- element.onEvent
- or self.onEvent
- or element.onTouchEvent
- or self.onTouchEvent
- or element.editable
- )
- and not element.disabled
- and self.touchEnabled
-
- if not canProcessEvents then
- if EventHandler._Performance and EventHandler._Performance.enabled then
- EventHandler._Performance:stopTimer("event_touch")
- end
- return
- end
-
- local bx = element.x
- local by = element.y
- local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
-
- -- Get current active touches from LÖVE
- local activeTouches = {}
- local touches = love.touch.getTouches()
- for _, id in ipairs(touches) do
- activeTouches[tostring(id)] = true
- end
-
- -- Count active tracked touches for multi-touch filtering
- local trackedTouchCount = 0
- for _ in pairs(self._touches) do
- trackedTouchCount = trackedTouchCount + 1
- end
-
- -- Process active touches
- for _, id in ipairs(touches) do
- local touchId = tostring(id)
- local tx, ty = love.touch.getPosition(id)
- local pressure = 1.0 -- LÖVE doesn't provide pressure by default
-
- -- Check if touch is within element bounds
- local isInside = tx >= bx and tx <= bx + bw and ty >= by and ty <= by + bh
-
- if isInside then
- if not self._touches[touchId] then
- -- Multi-touch filtering: reject new touches when multiTouchEnabled=false
- -- and we already have an active touch
- if self.multiTouchEnabled or trackedTouchCount == 0 then
- -- New touch began
- self:_handleTouchBegan(element, touchId, tx, ty, pressure)
- trackedTouchCount = trackedTouchCount + 1
- end
- else
- -- Touch moved
- self:_handleTouchMoved(element, touchId, tx, ty, pressure)
- end
- elseif self._touches[touchId] then
- -- Touch moved outside or ended
- if activeTouches[touchId] then
- -- Still active but outside - fire moved event
- self:_handleTouchMoved(element, touchId, tx, ty, pressure)
- else
- -- Touch ended
- self:_handleTouchEnded(element, touchId, tx, ty, pressure)
- end
- end
- end
-
- -- Check for ended touches (touches that were tracked but are no longer active)
- for touchId, _ in pairs(self._touches) do
- if not activeTouches[touchId] then
- -- Touch ended or cancelled
- local lastPos = self._lastTouchPositions[touchId]
- if lastPos then
- self:_handleTouchEnded(element, touchId, lastPos.x, lastPos.y, 1.0)
- else
- -- Cleanup orphaned touch
- self:_cleanupTouch(touchId)
- end
- end
- end
-
- -- Stop performance timing
- if EventHandler._Performance and EventHandler._Performance.enabled then
- EventHandler._Performance:stopTimer("event_touch")
- end
-end
-
---- Handle touch began event
----@param element Element The parent element
----@param touchId string Touch identifier
----@param x number Touch X position
----@param y number Touch Y position
----@param pressure number Touch pressure (0-1)
-function EventHandler:_handleTouchBegan(element, touchId, x, y, pressure)
- -- Create touch state
- self._touches[touchId] = {
- x = x,
- y = y,
- pressure = pressure,
- timestamp = love.timer.getTime(),
- phase = "began",
- }
-
- -- Record start position
- self._touchStartPositions[touchId] = { x = x, y = y }
- self._lastTouchPositions[touchId] = { x = x, y = y }
-
- -- Initialize touch history
- self._touchHistory[touchId] = { { x = x, y = y, timestamp = love.timer.getTime() } }
-
- -- Create and fire touch press event
- local touchEvent = EventHandler._InputEvent.fromTouch(touchId, x, y, "began", pressure)
- touchEvent.type = "touchpress"
- touchEvent.dx = 0
- touchEvent.dy = 0
- self:_invokeCallback(element, touchEvent)
- self:_invokeTouchCallback(element, touchEvent)
-end
-
---- Handle touch moved event
----@param element Element The parent element
----@param touchId string Touch identifier
----@param x number Touch X position
----@param y number Touch Y position
----@param pressure number Touch pressure (0-1)
-function EventHandler:_handleTouchMoved(element, touchId, x, y, pressure)
- local touchState = self._touches[touchId]
-
- if not touchState then
- -- Touch not tracked, ignore
- return
- end
-
- local lastPos = self._lastTouchPositions[touchId]
- if not lastPos or lastPos.x ~= x or lastPos.y ~= y then
- -- Touch position changed
- local startPos = self._touchStartPositions[touchId]
- local dx = x - startPos.x
- local dy = y - startPos.y
-
- -- Update touch state
- touchState.x = x
- touchState.y = y
- touchState.pressure = pressure
- touchState.phase = "moved"
-
- -- Update last position
- self._lastTouchPositions[touchId] = { x = x, y = y }
-
- -- Add to touch history (keep last 5 positions)
- local history = self._touchHistory[touchId] or {}
- table.insert(history, { x = x, y = y, timestamp = love.timer.getTime() })
- if #history > 5 then
- table.remove(history, 1)
- end
- self._touchHistory[touchId] = history
-
- -- Create and fire touch move event
- local touchEvent = EventHandler._InputEvent.fromTouch(touchId, x, y, "moved", pressure)
- touchEvent.type = "touchmove"
- touchEvent.dx = dx
- touchEvent.dy = dy
- self:_invokeCallback(element, touchEvent)
- self:_invokeTouchCallback(element, touchEvent)
- end
-end
-
---- Handle touch ended event
----@param element Element The parent element
----@param touchId string Touch identifier
----@param x number Touch X position
----@param y number Touch Y position
----@param pressure number Touch pressure (0-1)
-function EventHandler:_handleTouchEnded(element, touchId, x, y, pressure)
- local touchState = self._touches[touchId]
-
- if not touchState then
- -- Touch not tracked, ignore
- return
- end
-
- local startPos = self._touchStartPositions[touchId]
- local dx = x - startPos.x
- local dy = y - startPos.y
-
- -- Create and fire touch release event
- local touchEvent = EventHandler._InputEvent.fromTouch(touchId, x, y, "ended", pressure)
- touchEvent.type = "touchrelease"
- touchEvent.dx = dx
- touchEvent.dy = dy
- self:_invokeCallback(element, touchEvent)
- self:_invokeTouchCallback(element, touchEvent)
-
- -- Cleanup touch state
- self:_cleanupTouch(touchId)
-end
-
---- Cleanup touch state
----@param touchId string Touch ID
-function EventHandler:_cleanupTouch(touchId)
- self._touches[touchId] = nil
- self._touchStartPositions[touchId] = nil
- self._lastTouchPositions[touchId] = nil
- self._touchHistory[touchId] = nil
-end
-
---- Get active touches on this element
----@return table Active touches
-function EventHandler:getActiveTouches()
- return self._touches
-end
-
---- Reset scrollbar press flag (called each frame)
-function EventHandler:resetScrollbarPressFlag()
- self._scrollbarPressHandled = false
-end
-
---- Check if any mouse button is pressed
----@return boolean True if any button is pressed
-function EventHandler:isAnyButtonPressed()
- for _, pressed in pairs(self._pressed) do
- if pressed then
- return true
- end
- end
- return false
-end
-
---- Check if a specific button is pressed
----@param button number Mouse button (1=left, 2=right, 3=middle)
----@return boolean True if button is pressed
-function EventHandler:isButtonPressed(button)
- return self._pressed[button] == true
-end
-
---- Invoke the onEvent callback, optionally deferring it if onEventDeferred is true
----@param element Element The element that triggered the event
----@param event InputEvent The event data
-function EventHandler:_invokeCallback(element, event)
- -- Read onEvent from element (source of truth), fallback to handler cache for backwards compat
- local callback = element.onEvent or self.onEvent
- if not callback then
- return
- end
-
- if self.onEventDeferred then
- -- Get FlexLove module to defer the callback
- local FlexLove = package.loaded["FlexLove"] or package.loaded["libs.FlexLove"]
- if FlexLove and FlexLove.deferCallback then
- FlexLove.deferCallback(function()
- callback(element, event)
- end)
- else
- EventHandler._ErrorHandler:error("EventHandler", "SYS_003", {
- eventType = event.type,
- })
- end
- else
- callback(element, event)
- end
-end
-
---- Invoke the onTouchEvent callback, optionally deferring it
----@param element Element The element that triggered the event
----@param event InputEvent The touch event data
-function EventHandler:_invokeTouchCallback(element, event)
- -- Read onTouchEvent from element (source of truth), fallback to handler cache for backwards compat
- local callback = element.onTouchEvent or self.onTouchEvent
- if not callback then
- return
- end
-
- if self.onTouchEventDeferred then
- local FlexLove = package.loaded["FlexLove"] or package.loaded["libs.FlexLove"]
- if FlexLove and FlexLove.deferCallback then
- FlexLove.deferCallback(function()
- callback(element, event)
- end)
- else
- EventHandler._ErrorHandler:error("EventHandler", "SYS_003", {
- eventType = event.type,
- })
- end
- else
- callback(element, event)
- end
-end
-
---- Invoke the onGesture callback, optionally deferring it
----@param element Element The element that triggered the event
----@param gesture table The gesture data from GestureRecognizer
-function EventHandler:_invokeGestureCallback(element, gesture)
- -- Read onGesture from element (source of truth), fallback to handler cache for backwards compat
- local callback = element.onGesture or self.onGesture
- if not callback then
- return
- end
-
- if self.onGestureDeferred then
- local FlexLove = package.loaded["FlexLove"] or package.loaded["libs.FlexLove"]
- if FlexLove and FlexLove.deferCallback then
- FlexLove.deferCallback(function()
- callback(element, gesture)
- end)
- else
- EventHandler._ErrorHandler:error("EventHandler", "SYS_003", {
- gestureType = gesture.type,
- })
- end
- else
- callback(element, gesture)
- end
-end
-
-return EventHandler
diff --git a/libs/flexlove/modules/FocusIndicator.lua b/libs/flexlove/modules/FocusIndicator.lua
deleted file mode 100644
index ebbac8ad..00000000
--- a/libs/flexlove/modules/FocusIndicator.lua
+++ /dev/null
@@ -1,232 +0,0 @@
-local packageName = ... or "FocusIndicator"
-local modulePath = packageName:match("(.-)[^%.]+$")
-
-local function req(name)
- return require(modulePath .. name)
-end
-
-local FocusIndicator = {}
-
---- Configuration
----@type KeyboardNavigationFocusIndicatorConfig
-FocusIndicator.config = {
- enabled = true,
-
- --- Custom draw function to override default rendering
- ---@type function|nil
- --- Called with: element, bounds, style - return true to skip default drawing
- draw = nil,
-
- -- Appearance
- color = { 0.2, 0.6, 1.0, 0.8 }, -- Blue with 80% opacity
- lineWidth = 2,
- inset = -3, -- Negative value extends beyond element
- borderRadius = 4,
-
- -- Animation
- animationDuration = 0.15, -- Seconds for focus animation
- pulseEnabled = false, -- Enable pulsing animation
- pulseDuration = 1.0, -- Seconds per pulse cycle
- pulseScaleMin = 0.95, -- Minimum scale during pulse
- pulseScaleMax = 1.05, -- Maximum scale during pulse
-}
-
---- State
-FocusIndicator._focusedElement = nil
-FocusIndicator._animationProgress = 0
-FocusIndicator._pulsePhase = 0
-FocusIndicator._hidden = true
-FocusIndicator._deps = nil
-
---- Initialize FocusIndicator module
----@param deps table Dependencies table containing Context and Color modules
----@field deps.Context table Context module for getting focused element
----@field deps.Color table Color module for color manipulation
-function FocusIndicator.init(deps)
- FocusIndicator._deps = deps
- FocusIndicator._Context = deps.Context
- FocusIndicator._Color = deps.Color
-end
-
---- Update animation state for entrance and pulse effects
----@param dt number Delta time in seconds since last frame
-function FocusIndicator:update(dt)
- if not FocusIndicator.config.enabled then
- return
- end
-
- -- Update focus entrance animation
- if FocusIndicator._animationProgress < 1 then
- FocusIndicator._animationProgress =
- math.min(1, FocusIndicator._animationProgress + (dt / FocusIndicator.config.animationDuration))
- end
-
- -- Update pulse animation
- if FocusIndicator.config.pulseEnabled then
- FocusIndicator._pulsePhase = (FocusIndicator._pulsePhase + dt) % FocusIndicator.config.pulseDuration
- end
-end
-
---- Set the focused element to render indicator around
----@param element Element? The element to show focus indicator around, or nil to hide
-function FocusIndicator.setFocused(element)
- FocusIndicator._focusedElement = element
- FocusIndicator._hidden = element == nil
- -- Reset animation when focus changes
- if element then
- FocusIndicator._animationProgress = 0
- end
-end
-
---- Get the current scale factor for animations
---- Combines entrance scale (0.8 to 1.0) with optional pulse scale
----@return number Scale factor (typically 0.8-1.05 range)
-function FocusIndicator:getScale()
- local scale = 1
-
- -- Apply entrance animation (scale up from 0.8)
- local entranceScale = 0.8 + (0.2 * FocusIndicator._animationProgress)
- scale = scale * entranceScale
-
- -- Apply pulse animation
- if FocusIndicator.config.pulseEnabled then
- local pulseProgress = FocusIndicator._pulsePhase / FocusIndicator.config.pulseDuration
- -- Smooth sine wave pulse
- local pulseScale = FocusIndicator.config.pulseScaleMin
- + (FocusIndicator.config.pulseScaleMax - FocusIndicator.config.pulseScaleMin)
- * (0.5 + 0.5 * math.sin(2 * math.pi * pulseProgress))
- scale = scale * pulseScale
- end
-
- return scale
-end
-
---- Get the current opacity for the indicator
---- Applies entrance animation fade-in to the configured alpha
----@return number Alpha value (0-1 range)
-function FocusIndicator:getOpacity()
- -- Fade in on focus
- return FocusIndicator.config.color[4] * FocusIndicator._animationProgress
-end
-
---- Draw the focus indicator around the focused element
---- Renders a rounded rectangle border, or calls custom draw function if configured
---- Should be called from within love.draw() after all elements are drawn
-function FocusIndicator:draw()
- if not FocusIndicator.config.enabled then
- return
- end
-
- if FocusIndicator._hidden then
- return
- end
-
- -- In immediate mode the stored element reference is stale (recreated every frame).
- -- Always resolve through Context so we get the live object with up-to-date positions.
- local element
- if FocusIndicator._Context then
- element = FocusIndicator._Context.getFocused()
- else
- element = FocusIndicator._focusedElement
- end
-
- if not element then
- return
- end
-
- -- Get element dimensions (use border-box size which includes padding)
- local x = element.x or 0
- local y = element.y or 0
- local w = element._borderBoxWidth
- or (element.width + (element.padding and (element.padding.left + element.padding.right) or 0))
- local h = element._borderBoxHeight
- or (element.height + (element.padding and (element.padding.top + element.padding.bottom) or 0))
-
- if w == 0 or h == 0 then
- return
- end
-
- -- Calculate indicator dimensions with inset and scale
- local inset = FocusIndicator.config.inset
- local scale = self:getScale()
-
- local indicatorX = x + inset
- local indicatorY = y + inset
- local indicatorW = w - 2 * inset
- local indicatorH = h - 2 * inset
-
- -- Center the scale around the element
- local offsetX = (indicatorW * (1 - scale)) / 2
- local offsetY = (indicatorH * (1 - scale)) / 2
-
- indicatorX = indicatorX + offsetX
- indicatorY = indicatorY + offsetY
- indicatorW = indicatorW * scale
- indicatorH = indicatorH * scale
-
- -- Get color with animated opacity
- local r, g, b = FocusIndicator.config.color[1], FocusIndicator.config.color[2], FocusIndicator.config.color[3]
- local a = self:getOpacity()
-
- -- Build style table for custom draw callback
- local bounds = {
- x = indicatorX,
- y = indicatorY,
- width = indicatorW,
- height = indicatorH,
- }
-
- local style = {
- color = { r = r, g = g, b = b, a = a },
- lineWidth = FocusIndicator.config.lineWidth,
- borderRadius = FocusIndicator.config.borderRadius,
- scale = scale,
- opacity = a,
- }
-
- -- Check for custom draw callback
- if FocusIndicator.config.draw then
- local skipDefault = FocusIndicator.config.draw(element, bounds, style)
- if skipDefault then
- return
- end
- end
-
- -- Save current love.graphics state
- local prevBlend, prevAlphaMode = love.graphics.getBlendMode()
- local prevR, prevG, prevB, prevA = love.graphics.getColor()
- local prevLineWidth = love.graphics.getLineWidth()
-
- -- Set blend mode for transparency
- love.graphics.setBlendMode("alpha")
-
- -- Draw rounded rectangle border
- love.graphics.setColor(r, g, b, a)
- love.graphics.setLineWidth(FocusIndicator.config.lineWidth)
-
- -- Draw the rounded rectangle border
- local borderRadius = FocusIndicator.config.borderRadius
- love.graphics.rectangle("line", indicatorX, indicatorY, indicatorW, indicatorH, borderRadius)
-
- -- Restore love.graphics state
- love.graphics.setBlendMode(prevBlend, prevAlphaMode)
- love.graphics.setColor(prevR, prevG, prevB, prevA)
- love.graphics.setLineWidth(prevLineWidth)
-end
-
---- Set the indicator color
----@param r number Red component (0-1 range)
----@param g number Green component (0-1 range)
----@param b number Blue component (0-1 range)
----@param a number|nil Alpha component (0-1 range), defaults to current alpha if omitted
-function FocusIndicator.setColor(r, g, b, a)
- FocusIndicator.config.color = { r, g, b, a or FocusIndicator.config.color[4] }
-end
-
---- Set the stroke width for the indicator border
----@param width number Line width in pixels
-function FocusIndicator.setLineWidth(width)
- FocusIndicator.config.lineWidth = width
-end
-
-return FocusIndicator
diff --git a/libs/flexlove/modules/FontCache.lua b/libs/flexlove/modules/FontCache.lua
deleted file mode 100644
index 7aef441c..00000000
--- a/libs/flexlove/modules/FontCache.lua
+++ /dev/null
@@ -1,269 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local function req(name)
- return require(modulePath .. name)
-end
-
--- Font cache with LRU eviction, font resolution, and cache management.
--- `ErrorHandler` and `resolveImagePath` are injected via init() to avoid
--- a cross-import into utils (utils re-exports the cache via aliases).
-
--- Font cache with LRU eviction
-local FONT_CACHE = {}
-local FONT_CACHE_MAX_SIZE = 50
-local FONT_CACHE_STATS = {
- hits = 0,
- misses = 0,
- evictions = 0,
- size = 0,
-}
-
-local ErrorHandler = nil
-local resolveImagePath = nil
-
---- Initialize dependencies
----@param deps table Dependencies: { ErrorHandler = ErrorHandler, resolveImagePath = function }
-local function init(deps)
- if type(deps) == "table" then
- ErrorHandler = deps.ErrorHandler
- resolveImagePath = deps.resolveImagePath
- end
-end
-
--- LRU tracking: each entry has {font, lastUsed, accessCount}
-local function updateCacheAccess(cacheKey)
- local entry = FONT_CACHE[cacheKey]
- if entry then
- entry.lastUsed = love.timer.getTime()
- entry.accessCount = entry.accessCount + 1
- end
-end
-
-local function evictLRU()
- local oldestKey = nil
- local oldestTime = math.huge
-
- for key, entry in pairs(FONT_CACHE) do
- -- Skip methods (get, getFont) - only evict cache entries (tables with lastUsed)
- if type(entry) == "table" and entry.lastUsed then
- if entry.lastUsed < oldestTime then
- oldestTime = entry.lastUsed
- oldestKey = key
- end
- end
- end
-
- if oldestKey then
- FONT_CACHE[oldestKey] = nil
- FONT_CACHE_STATS.evictions = FONT_CACHE_STATS.evictions + 1
- FONT_CACHE_STATS.size = FONT_CACHE_STATS.size - 1
- end
-end
-
---- Create or get a font from cache
----@param size number
----@param fontPath string?
----@return love.Font
-function FONT_CACHE.get(size, fontPath)
- -- Bucket font sizes for better cache reuse (reduces unique cache entries)
- -- Small sizes (< 20): round to nearest 2
- -- Medium sizes (20-40): round to nearest 4
- -- Large sizes (> 40): round to nearest 8
- if size < 20 then
- size = math.floor((size + 1) / 2) * 2
- elseif size < 40 then
- size = math.floor((size + 2) / 4) * 4
- else
- size = math.floor((size + 4) / 8) * 8
- end
-
- local cacheKey = fontPath and (fontPath .. ":" .. tostring(size)) or ("default:" .. tostring(size))
-
- if FONT_CACHE[cacheKey] then
- -- Cache hit
- FONT_CACHE_STATS.hits = FONT_CACHE_STATS.hits + 1
- updateCacheAccess(cacheKey)
- return FONT_CACHE[cacheKey].font
- end
-
- -- Cache miss
- FONT_CACHE_STATS.misses = FONT_CACHE_STATS.misses + 1
-
- local font
- if fontPath then
- local resolvedPath = resolveImagePath(fontPath)
- local success, result = pcall(love.graphics.newFont, resolvedPath, size)
- if success then
- font = result
- else
- if ErrorHandler then
- ErrorHandler:warn("utils", "RES_004", {
- resourceType = "font",
- path = fontPath,
- })
- end
- font = love.graphics.newFont(size)
- end
- else
- font = love.graphics.newFont(size)
- end
-
- -- Add to cache with LRU metadata
- FONT_CACHE[cacheKey] = {
- font = font,
- lastUsed = love.timer.getTime(),
- accessCount = 1,
- }
- FONT_CACHE_STATS.size = FONT_CACHE_STATS.size + 1
-
- -- Evict if cache is full
- if FONT_CACHE_STATS.size > FONT_CACHE_MAX_SIZE then
- evictLRU()
- end
-
- return font
-end
-
---- Get font for text size (cached)
----@param textSize number?
----@param fontPath string?
----@return love.Font
-function FONT_CACHE.getFont(textSize, fontPath)
- if textSize then
- return FONT_CACHE.get(textSize, fontPath)
- else
- return love.graphics.getFont()
- end
-end
-
--- Font resolution utilities
-
---- Resolve font path from fontFamily and theme
----@param fontFamily string? Font family name or direct path
----@param themeComponent string? Theme component name
----@param themeManager table? ThemeManager instance
----@return string? Resolved font path or nil
-local function resolveFontPath(fontFamily, themeComponent, themeManager)
- if fontFamily then
- -- Check if fontFamily is a theme font name
- local themeToUse = themeManager and themeManager:getTheme()
- if themeToUse and themeToUse.fonts and themeToUse.fonts[fontFamily] then
- return themeToUse.fonts[fontFamily]
- else
- -- Treat as direct path to font file
- return fontFamily
- end
- elseif themeComponent and themeManager then
- -- If using themeComponent but no fontFamily specified, check for default font in theme
- return themeManager:getDefaultFontFamily()
- end
- return nil
-end
-
---- Get font for element (resolves from theme or fontFamily)
----@param textSize number? Text size in pixels
----@param fontFamily string? Font family name or direct path
----@param themeComponent string? Theme component name
----@param themeManager table? ThemeManager instance
----@return love.Font
-local function getFont(textSize, fontFamily, themeComponent, themeManager)
- local fontPath = resolveFontPath(fontFamily, themeComponent, themeManager)
- return FONT_CACHE.getFont(textSize, fontPath)
-end
-
--- Font cache management
-
---- Get font cache statistics
----@return table stats {hits, misses, evictions, size, hitRate}
-local function getFontCacheStats()
- local total = FONT_CACHE_STATS.hits + FONT_CACHE_STATS.misses
- local hitRate = total > 0 and (FONT_CACHE_STATS.hits / total) or 0
- return {
- hits = FONT_CACHE_STATS.hits,
- misses = FONT_CACHE_STATS.misses,
- evictions = FONT_CACHE_STATS.evictions,
- size = FONT_CACHE_STATS.size,
- hitRate = hitRate,
- }
-end
-
---- Set maximum font cache size
----@param maxSize number Maximum number of fonts to cache
-local function setFontCacheSize(maxSize)
- FONT_CACHE_MAX_SIZE = math.max(1, maxSize)
-
- -- Evict entries if cache is now over limit
- while FONT_CACHE_STATS.size > FONT_CACHE_MAX_SIZE do
- evictLRU()
- end
-end
-
---- Clear font cache
-local function clearFontCache()
- -- Clear cache entries but preserve methods (get, getFont)
- for key, entry in pairs(FONT_CACHE) do
- if type(entry) == "table" and entry.lastUsed then
- FONT_CACHE[key] = nil
- end
- end
- FONT_CACHE_STATS.size = 0
- FONT_CACHE_STATS.evictions = 0
-end
-
---- Preload font at multiple sizes
----@param fontPath string? Path to font file (nil for default font)
----@param sizes table Array of font sizes to preload
-local function preloadFont(fontPath, sizes)
- for _, size in ipairs(sizes) do
- -- Round size to reduce cache entries
- size = math.floor(size + 0.5)
-
- local cacheKey = fontPath and (fontPath .. ":" .. tostring(size)) or ("default:" .. tostring(size))
-
- if not FONT_CACHE[cacheKey] then
- local font
- if fontPath then
- local resolvedPath = resolveImagePath(fontPath)
- local success, result = pcall(love.graphics.newFont, resolvedPath, size)
- if success then
- font = result
- else
- font = love.graphics.newFont(size)
- end
- else
- font = love.graphics.newFont(size)
- end
-
- FONT_CACHE[cacheKey] = {
- font = font,
- lastUsed = love.timer.getTime(),
- accessCount = 1,
- }
- FONT_CACHE_STATS.size = FONT_CACHE_STATS.size + 1
- FONT_CACHE_STATS.misses = FONT_CACHE_STATS.misses + 1
-
- -- Evict if cache is full
- if FONT_CACHE_STATS.size > FONT_CACHE_MAX_SIZE then
- evictLRU()
- end
- end
- end
-end
-
---- Reset font cache statistics
-local function resetFontCacheStats()
- FONT_CACHE_STATS.hits = 0
- FONT_CACHE_STATS.misses = 0
- FONT_CACHE_STATS.evictions = 0
-end
-
-return {
- FONT_CACHE = FONT_CACHE,
- init = init,
- resolveFontPath = resolveFontPath,
- getFont = getFont,
- getFontCacheStats = getFontCacheStats,
- setFontCacheSize = setFontCacheSize,
- clearFontCache = clearFontCache,
- preloadFont = preloadFont,
- resetFontCacheStats = resetFontCacheStats,
-}
diff --git a/libs/flexlove/modules/GestureRecognizer.lua b/libs/flexlove/modules/GestureRecognizer.lua
deleted file mode 100644
index 5e324f09..00000000
--- a/libs/flexlove/modules/GestureRecognizer.lua
+++ /dev/null
@@ -1,583 +0,0 @@
----@class GestureRecognizer
----@field _touches table -- Current touch states
----@field _gestureStates table -- Active gesture states
----@field _config table -- Gesture configuration (thresholds, etc.)
----@field _InputEvent table
----@field _utils table
-local GestureRecognizer = {}
-GestureRecognizer.__index = GestureRecognizer
-
--- Gesture types enum
-local GestureType = {
- TAP = "tap",
- DOUBLE_TAP = "double_tap",
- LONG_PRESS = "long_press",
- SWIPE = "swipe",
- PAN = "pan",
- PINCH = "pinch",
- ROTATE = "rotate",
-}
-
--- Gesture states
-local GestureState = {
- POSSIBLE = "possible",
- BEGAN = "began",
- CHANGED = "changed",
- ENDED = "ended",
- CANCELLED = "cancelled",
- FAILED = "failed",
-}
-
--- Default configuration
-local defaultConfig = {
- -- Tap gesture
- tapMaxDuration = 0.3, -- seconds
- tapMaxMovement = 10, -- pixels
-
- -- Double-tap gesture
- doubleTapInterval = 0.3, -- seconds between taps
-
- -- Long-press gesture
- longPressMinDuration = 0.5, -- seconds
- longPressMaxMovement = 10, -- pixels
-
- -- Swipe gesture
- swipeMinDistance = 50, -- pixels
- swipeMaxDuration = 0.2, -- seconds
- swipeMinVelocity = 200, -- pixels per second
-
- -- Pan gesture
- panMinMovement = 5, -- pixels to start pan
-
- -- Pinch gesture
- pinchMinScaleChange = 0.1, -- 10% scale change
-
- -- Rotate gesture
- rotateMinAngleChange = 5, -- degrees
-}
-
---- Create a new GestureRecognizer instance
----@param config table? Optional configuration options
----@param deps table Dependencies {InputEvent, utils}
----@return GestureRecognizer
-function GestureRecognizer.new(config, deps)
- config = config or {}
-
- local self = setmetatable({}, GestureRecognizer)
-
- self._InputEvent = deps.InputEvent
- self._utils = deps.utils
-
- -- Merge configuration with defaults
- self._config = {}
- for key, value in pairs(defaultConfig) do
- self._config[key] = config[key] or value
- end
-
- self._touches = {}
- self._gestureStates = {
- tap = nil,
- doubleTap = { lastTapTime = 0, tapCount = 0 },
- longPress = {},
- swipe = {},
- pan = {},
- pinch = {},
- rotate = {},
- }
-
- return self
-end
-
---- Update gesture recognizer with touch event
----@param event InputEvent Touch event
-function GestureRecognizer:processTouchEvent(event)
- if not event.touchId then
- return nil
- end
-
- local touchId = event.touchId
- local gestures = {}
-
- -- Update touch state
- if event.type == "touchpress" then
- self._touches[touchId] = {
- startX = event.x,
- startY = event.y,
- x = event.x,
- y = event.y,
- startTime = event.timestamp,
- lastTime = event.timestamp,
- phase = "began",
- }
-
- -- Initialize gesture detection
- self:_detectTapBegan(touchId, event)
- self:_detectLongPressBegan(touchId, event)
- elseif event.type == "touchmove" then
- local touch = self._touches[touchId]
- if touch then
- touch.x = event.x
- touch.y = event.y
- touch.lastTime = event.timestamp
- touch.phase = "moved"
-
- -- Update gesture detection
- local panGesture = self:_detectPan(touchId, event)
- if panGesture then
- table.insert(gestures, panGesture)
- end
- local swipeGesture = self:_detectSwipe(touchId, event)
- if swipeGesture then
- table.insert(gestures, swipeGesture)
- end
-
- -- Multi-touch gestures
- if self:_getTouchCount() >= 2 then
- local pinchGesture = self:_detectPinch(event)
- if pinchGesture then
- table.insert(gestures, pinchGesture)
- end
- local rotateGesture = self:_detectRotate(event)
- if rotateGesture then
- table.insert(gestures, rotateGesture)
- end
- end
- end
- elseif event.type == "touchrelease" then
- local touch = self._touches[touchId]
- if touch then
- touch.phase = "ended"
-
- -- Finalize gesture detection
- local tapGesture = self:_detectTapEnded(touchId, event)
- if tapGesture then
- table.insert(gestures, tapGesture)
- end
- local swipeGesture = self:_detectSwipeEnded(touchId, event)
- if swipeGesture then
- table.insert(gestures, swipeGesture)
- end
- local panGesture = self:_detectPanEnded(touchId, event)
- if panGesture then
- table.insert(gestures, panGesture)
- end
-
- -- Cleanup touch
- self._touches[touchId] = nil
- end
- elseif event.type == "touchcancel" then
- -- Cancel all active gestures for this touch
- self._touches[touchId] = nil
- self:_cancelAllGestures()
- end
-
- return #gestures > 0 and gestures or nil
-end
-
---- Get number of active touches
----@return number
-function GestureRecognizer:_getTouchCount()
- local count = 0
- for _ in pairs(self._touches) do
- count = count + 1
- end
- return count
-end
-
---- Detect tap gesture began
----@param touchId string
----@param event InputEvent
-function GestureRecognizer:_detectTapBegan(touchId, event)
- -- Tap detection happens on touch end
- -- Just record the touch for now
-end
-
---- Detect tap gesture ended
----@param touchId string
----@param event InputEvent
-function GestureRecognizer:_detectTapEnded(touchId, event)
- local touch = self._touches[touchId]
- if not touch then
- return
- end
-
- local duration = event.timestamp - touch.startTime
- local dx = event.x - touch.startX
- local dy = event.y - touch.startY
- local distance = math.sqrt(dx * dx + dy * dy)
-
- -- Check if it's a valid tap
- if duration < self._config.tapMaxDuration and distance < self._config.tapMaxMovement then
- local currentTime = event.timestamp
- local doubleTapState = self._gestureStates.doubleTap
-
- -- Check for double-tap
- if currentTime - doubleTapState.lastTapTime < self._config.doubleTapInterval then
- doubleTapState.tapCount = doubleTapState.tapCount + 1
-
- if doubleTapState.tapCount >= 2 then
- -- Fire double-tap gesture
- return {
- type = GestureType.DOUBLE_TAP,
- state = GestureState.ENDED,
- x = event.x,
- y = event.y,
- timestamp = event.timestamp,
- }
- end
- else
- doubleTapState.tapCount = 1
- end
-
- doubleTapState.lastTapTime = currentTime
-
- -- Fire tap gesture
- return {
- type = GestureType.TAP,
- state = GestureState.ENDED,
- x = event.x,
- y = event.y,
- timestamp = event.timestamp,
- }
- end
-end
-
---- Detect long-press gesture began
----@param touchId string
----@param event InputEvent
-function GestureRecognizer:_detectLongPressBegan(touchId, event)
- -- Long-press detection happens continuously during touch
- self._gestureStates.longPress[touchId] = {
- startX = event.x,
- startY = event.y,
- startTime = event.timestamp,
- triggered = false,
- }
-end
-
---- Detect pan gesture
----@param touchId string
----@param event InputEvent
----@return table? Gesture event
-function GestureRecognizer:_detectPan(touchId, event)
- local touch = self._touches[touchId]
- if not touch then
- return nil
- end
-
- local dx = event.x - touch.startX
- local dy = event.y - touch.startY
- local distance = math.sqrt(dx * dx + dy * dy)
-
- local panState = self._gestureStates.pan[touchId]
-
- if not panState then
- -- Check if pan should begin
- if distance >= self._config.panMinMovement then
- self._gestureStates.pan[touchId] = {
- active = true,
- lastX = touch.startX,
- lastY = touch.startY,
- }
- panState = self._gestureStates.pan[touchId]
-
- return {
- type = GestureType.PAN,
- state = GestureState.BEGAN,
- x = event.x,
- y = event.y,
- dx = dx,
- dy = dy,
- timestamp = event.timestamp,
- }
- end
- else
- -- Pan is active, fire changed event
- local panDx = event.x - panState.lastX
- local panDy = event.y - panState.lastY
-
- panState.lastX = event.x
- panState.lastY = event.y
-
- return {
- type = GestureType.PAN,
- state = GestureState.CHANGED,
- x = event.x,
- y = event.y,
- dx = panDx,
- dy = panDy,
- totalDx = dx,
- totalDy = dy,
- timestamp = event.timestamp,
- }
- end
-
- return nil
-end
-
---- Detect pan ended
----@param touchId string
----@param event InputEvent
----@return table? Gesture event
-function GestureRecognizer:_detectPanEnded(touchId, event)
- local panState = self._gestureStates.pan[touchId]
- if panState and panState.active then
- self._gestureStates.pan[touchId] = nil
-
- local touch = self._touches[touchId]
- local dx = event.x - touch.startX
- local dy = event.y - touch.startY
-
- return {
- type = GestureType.PAN,
- state = GestureState.ENDED,
- x = event.x,
- y = event.y,
- dx = dx,
- dy = dy,
- timestamp = event.timestamp,
- }
- end
-
- return nil
-end
-
---- Detect swipe gesture
----@param touchId string
----@param event InputEvent
-function GestureRecognizer:_detectSwipe(touchId, event)
- -- Swipe detection happens on touch end
-end
-
---- Detect swipe ended
----@param touchId string
----@param event InputEvent
----@return table? Gesture event
-function GestureRecognizer:_detectSwipeEnded(touchId, event)
- local touch = self._touches[touchId]
- if not touch then
- return nil
- end
-
- local duration = event.timestamp - touch.startTime
- local dx = event.x - touch.startX
- local dy = event.y - touch.startY
- local distance = math.sqrt(dx * dx + dy * dy)
-
- -- Check if it's a valid swipe
- if distance >= self._config.swipeMinDistance and duration <= self._config.swipeMaxDuration then
- local velocity = distance / duration
-
- if velocity >= self._config.swipeMinVelocity then
- -- Determine swipe direction
- local angle = math.atan2(dy, dx)
- local direction = "right"
-
- if angle >= -math.pi / 4 and angle < math.pi / 4 then
- direction = "right"
- elseif angle >= math.pi / 4 and angle < 3 * math.pi / 4 then
- direction = "down"
- elseif angle >= -3 * math.pi / 4 and angle < -math.pi / 4 then
- direction = "up"
- else
- direction = "left"
- end
-
- return {
- type = GestureType.SWIPE,
- state = GestureState.ENDED,
- x = event.x,
- y = event.y,
- dx = dx,
- dy = dy,
- direction = direction,
- velocity = velocity,
- timestamp = event.timestamp,
- }
- end
- end
-
- return nil
-end
-
---- Detect pinch gesture
----@param event InputEvent
----@return table? Gesture event
-function GestureRecognizer:_detectPinch(event)
- -- Get two touches for pinch
- local touches = {}
- for touchId, touch in pairs(self._touches) do
- table.insert(touches, { id = touchId, touch = touch })
- if #touches >= 2 then
- break
- end
- end
-
- if #touches < 2 then
- return nil
- end
-
- local t1 = touches[1].touch
- local t2 = touches[2].touch
-
- -- Calculate current distance
- local currentDx = t2.x - t1.x
- local currentDy = t2.y - t1.y
- local currentDistance = math.sqrt(currentDx * currentDx + currentDy * currentDy)
-
- -- Calculate initial distance
- local initialDx = t2.startX - t1.startX
- local initialDy = t2.startY - t1.startY
- local initialDistance = math.sqrt(initialDx * initialDx + initialDy * initialDy)
-
- if initialDistance == 0 then
- return nil
- end
-
- -- Calculate scale
- local scale = currentDistance / initialDistance
- local pinchState = self._gestureStates.pinch
-
- if not pinchState.active then
- -- Check if pinch should begin
- if math.abs(scale - 1.0) >= self._config.pinchMinScaleChange then
- pinchState.active = true
- pinchState.initialScale = scale
- pinchState.lastScale = scale
-
- -- Calculate center point
- local centerX = (t1.x + t2.x) / 2
- local centerY = (t1.y + t2.y) / 2
-
- return {
- type = GestureType.PINCH,
- state = GestureState.BEGAN,
- scale = scale,
- centerX = centerX,
- centerY = centerY,
- timestamp = event.timestamp,
- }
- end
- else
- -- Pinch is active, fire changed event
- local centerX = (t1.x + t2.x) / 2
- local centerY = (t1.y + t2.y) / 2
-
- local scaleChange = scale - pinchState.lastScale
- pinchState.lastScale = scale
-
- return {
- type = GestureType.PINCH,
- state = GestureState.CHANGED,
- scale = scale,
- scaleChange = scaleChange,
- centerX = centerX,
- centerY = centerY,
- timestamp = event.timestamp,
- }
- end
-
- return nil
-end
-
---- Detect rotate gesture
----@param event InputEvent
----@return table? Gesture event
-function GestureRecognizer:_detectRotate(event)
- -- Get two touches for rotation
- local touches = {}
- for touchId, touch in pairs(self._touches) do
- table.insert(touches, { id = touchId, touch = touch })
- if #touches >= 2 then
- break
- end
- end
-
- if #touches < 2 then
- return nil
- end
-
- local t1 = touches[1].touch
- local t2 = touches[2].touch
-
- -- Calculate current angle
- local currentAngle = math.atan2(t2.y - t1.y, t2.x - t1.x)
-
- -- Calculate initial angle
- local initialAngle = math.atan2(t2.startY - t1.startY, t2.startX - t1.startX)
-
- -- Calculate rotation (in degrees)
- local rotation = (currentAngle - initialAngle) * 180 / math.pi
-
- local rotateState = self._gestureStates.rotate
-
- if not rotateState.active then
- -- Check if rotation should begin
- if math.abs(rotation) >= self._config.rotateMinAngleChange then
- rotateState.active = true
- rotateState.initialRotation = rotation
- rotateState.lastRotation = rotation
-
- -- Calculate center point
- local centerX = (t1.x + t2.x) / 2
- local centerY = (t1.y + t2.y) / 2
-
- return {
- type = GestureType.ROTATE,
- state = GestureState.BEGAN,
- rotation = rotation,
- centerX = centerX,
- centerY = centerY,
- timestamp = event.timestamp,
- }
- end
- else
- -- Rotation is active, fire changed event
- local centerX = (t1.x + t2.x) / 2
- local centerY = (t1.y + t2.y) / 2
-
- local rotationChange = rotation - rotateState.lastRotation
- rotateState.lastRotation = rotation
-
- return {
- type = GestureType.ROTATE,
- state = GestureState.CHANGED,
- rotation = rotation,
- rotationChange = rotationChange,
- centerX = centerX,
- centerY = centerY,
- timestamp = event.timestamp,
- }
- end
-
- return nil
-end
-
---- Cancel all active gestures
-function GestureRecognizer:_cancelAllGestures()
- for gestureType, state in pairs(self._gestureStates) do
- if type(state) == "table" and state.active then
- state.active = false
- end
- end
-end
-
---- Reset gesture recognizer state
-function GestureRecognizer:reset()
- self._touches = {}
- self._gestureStates = {
- tap = nil,
- doubleTap = { lastTapTime = 0, tapCount = 0 },
- longPress = {},
- swipe = {},
- pan = {},
- pinch = { active = false },
- rotate = { active = false },
- }
-end
-
--- Export gesture types and states
-GestureRecognizer.GestureType = GestureType
-GestureRecognizer.GestureState = GestureState
-
-return GestureRecognizer
diff --git a/libs/flexlove/modules/Grid.lua b/libs/flexlove/modules/Grid.lua
deleted file mode 100644
index d37600c7..00000000
--- a/libs/flexlove/modules/Grid.lua
+++ /dev/null
@@ -1,336 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local utils = require(modulePath .. "utils")
-local enums = utils.enums
-local Units = require(modulePath .. "Units")
-
-local Positioning = enums.Positioning
-local AlignItems = enums.AlignItems
-
---- Grid layout with variable column widths / row heights
---- Supports px, %, fr, auto, vw, vh, and calc track sizes
-local Grid = {}
-
---- Parse a single track spec into {type, value}
---- Uses the Units pipeline for standard CSS units (px, %, vw, vh, calc).
---- Grid-specific types (fr, auto) are handled directly.
----@param spec number|string Track specification: number (px), string ("100px", "50%", "10vw", "1fr", "auto")
----@param availableSize number Container size for % resolution
----@param viewportWidth number Viewport width for vw resolution
----@param viewportHeight number Viewport height for vh resolution
----@return table {type: "px"|"fr"|"auto", value: number}
-function Grid._parseTrack(spec, availableSize, viewportWidth, viewportHeight)
- -- Handle calc objects (tables with _isCalc flag from FlexLove.calc())
- if type(spec) == "table" then
- local resolved = Units.resolve(spec, "calc", viewportWidth, viewportHeight, availableSize)
- return { type = "px", value = resolved }
- end
-
- if type(spec) == "number" then
- return { type = "px", value = spec }
- end
-
- if type(spec) == "string" then
- if spec == "auto" then
- return { type = "auto", value = 0 }
- end
-
- -- Check for fr unit (grid-specific, not in Units pipeline)
- local numStr, unit = spec:match("^([%-]?[%d%.]+)(.*)$")
- if numStr and unit == "fr" then
- local num = tonumber(numStr)
- if num then
- return { type = "fr", value = num }
- end
- end
-
- -- Delegate all other units to the Units pipeline (px, %, vw, vh, calc)
- local parsedVal, parsedUnit = Units.parse(spec)
- local resolved = Units.resolve(parsedVal, parsedUnit, viewportWidth, viewportHeight, availableSize)
- return { type = "px", value = resolved }
- end
-
- -- Default: 1fr
- return { type = "fr", value = 1 }
-end
-
---- Build track list from gridColumns/gridRows or fall back to equal 1fr tracks
----@param spec number|table? Track count (number = equal 1fr tracks) or array of track specs (e.g., {"1fr", "2fr", "100px"})
----@param availableSize number Container size for % resolution
----@param viewportWidth number Viewport width for vw resolution
----@param viewportHeight number Viewport height for vh resolution
----@return table Array of {type, value} track descriptors
-function Grid._buildTracks(spec, availableSize, viewportWidth, viewportHeight)
- if type(spec) == "table" and #spec > 0 then
- local tracks = {}
- for i, s in ipairs(spec) do
- tracks[i] = Grid._parseTrack(s, availableSize, viewportWidth, viewportHeight)
- end
- return tracks
- end
- -- Fallback: equal 1fr tracks
- local count = (type(spec) == "number" and spec > 0) and spec or 1
- local tracks = {}
- for i = 1, count do
- tracks[i] = { type = "fr", value = 1 }
- end
- return tracks
-end
-
---- Measure intrinsic content sizes for auto tracks
---- Maps children to their tracks and computes each child's max-content contribution.
---- For children with explicit dimensions (units unit ~= "auto"), uses the original
---- explicit size. For auto-sized children, uses calculated content size.
---- Stores the max per auto track. Matches CSS Grid auto sizing where tracks size
---- to the max-content contribution of their grid items.
----@param tracks table Array of {type, value} track descriptors
----@param children table Array of grid child elements
----@param axis "width"|"height" Dimension axis to measure
-function Grid._measureAutoTracks(tracks, children, axis)
- local trackSizes = {}
- local numTracks = #tracks
-
- for i, child in ipairs(children) do
- local index = i - 1
- local trackIdx = (index % numTracks) + 1
-
- local intrinsicSize
- if axis == "width" then
- local unit = child.units and child.units.width and child.units.width.unit
- if unit and unit ~= "auto" then
- -- Explicit width: use original value + padding (not stretched border-box)
- intrinsicSize = (child.units.width.value or 0) + child.padding.left + child.padding.right
- else
- -- Auto-sized: use calculated content size
- intrinsicSize = child:calculateAutoWidth()
- end
- else
- local unit = child.units and child.units.height and child.units.height.unit
- if unit and unit ~= "auto" then
- intrinsicSize = (child.units.height.value or 0) + child.padding.top + child.padding.bottom
- else
- intrinsicSize = child:calculateAutoHeight()
- end
- end
-
- if intrinsicSize > 0 then
- trackSizes[trackIdx] = math.max(trackSizes[trackIdx] or 0, intrinsicSize)
- end
- end
-
- -- Apply measured sizes to auto tracks
- for i, track in ipairs(tracks) do
- if track.type == "auto" and trackSizes[i] then
- track.value = trackSizes[i]
- end
- end
-end
-
---- Resolve track sizes: auto (content) first, then px (fixed), then fr (remaining)
---- CSS Grid algorithm:
---- 1. auto tracks size to their content (max-content) — measured by _measureAutoTracks
---- 2. px tracks consume their fixed size
---- 3. fr tracks consume remaining free space proportionally
---- 4. If no fr tracks exist, auto tracks share remaining space equally
---- Mutates tracks in-place, converting all to {type="px", value=number}
----@param tracks table Array of {type, value} track descriptors
----@param availableSize number Total space available for tracks
----@param gap number Gap between tracks
-function Grid._resolveTracks(tracks, availableSize, gap)
- local count = #tracks
- local totalGaps = (count > 1 and (count - 1) * gap) or 0
- local remaining = math.max(0, availableSize - totalGaps)
-
- -- Pass 1: Treat auto tracks as fixed (content-measured) and subtract
- for _, track in ipairs(tracks) do
- if track.type == "px" then
- remaining = remaining - track.value
- elseif track.type == "auto" then
- remaining = remaining - math.max(0, track.value)
- end
- end
-
- remaining = math.max(0, remaining)
-
- -- Pass 2: Count fr shares
- local totalFr = 0
- local autoCount = 0
- for _, track in ipairs(tracks) do
- if track.type == "fr" then
- totalFr = totalFr + track.value
- elseif track.type == "auto" then
- autoCount = autoCount + 1
- end
- end
-
- -- Pass 3: Distribute remaining space
- if totalFr > 0 then
- -- fr tracks consume all remaining free space
- local frUnit = remaining / totalFr
- for _, track in ipairs(tracks) do
- if track.type == "fr" then
- track.value = frUnit * track.value
- track.type = "px"
- end
- end
- elseif autoCount > 0 then
- -- No fr tracks: auto tracks share remaining space equally (grow beyond content)
- local extraPerAuto = math.max(0, remaining) / autoCount
- for _, track in ipairs(tracks) do
- if track.type == "auto" then
- track.value = track.value + extraPerAuto
- track.type = "px"
- end
- end
- end
-end
-
---- Layout grid items within a grid container
---- Supports variable column widths and row heights via gridColumns/gridRows (number or track specs)
---- Falls back to equal-sized 1fr tracks when nil
----@param element Element -- Grid container element
-function Grid.layoutGridItems(element)
- -- Calculate space reserved by absolutely positioned siblings
- local reservedLeft = 0
- local reservedRight = 0
- local reservedTop = 0
- local reservedBottom = 0
-
- for _, child in ipairs(element.children) do
- -- Only consider absolutely positioned children with explicit positioning and display != false
- if child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute and child.display ~= false then
- -- BORDER-BOX MODEL: Use border-box dimensions for space calculations
- local childBorderBoxWidth = child:getBorderBoxWidth()
- local childBorderBoxHeight = child:getBorderBoxHeight()
-
- if child.left then
- reservedLeft = math.max(reservedLeft, child.left + childBorderBoxWidth)
- end
- if child.right then
- reservedRight = math.max(reservedRight, child.right + childBorderBoxWidth)
- end
- if child.top then
- reservedTop = math.max(reservedTop, child.top + childBorderBoxHeight)
- end
- if child.bottom then
- reservedBottom = math.max(reservedBottom, child.bottom + childBorderBoxHeight)
- end
- end
- end
-
- -- Calculate available space (accounting for padding and reserved space)
- -- BORDER-BOX MODEL: element.width and element.height are already content dimensions
- local availableWidth = math.max(0, element.width - reservedLeft - reservedRight)
- local availableHeight = math.max(0, element.height - reservedTop - reservedBottom)
-
- -- Get gaps
- local columnGap = element.columnGap or 0
- local rowGap = element.rowGap or 0
-
- -- Collect grid children (exclude explicitly absolute and display=false)
- local gridChildren = {}
- for _, child in ipairs(element.children) do
- if not (child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute) and child.display ~= false then
- table.insert(gridChildren, child)
- end
- end
-
- -- Get viewport dimensions for unit resolution (vw, vh, %)
- local vpw, vph = Units.getViewport()
-
- -- Build tracks, measure auto tracks by content, then resolve sizes
- local colTracks = Grid._buildTracks(element.gridColumns, availableWidth, vpw, vph)
- local rowTracks = Grid._buildTracks(element.gridRows, availableHeight, vpw, vph)
-
- Grid._measureAutoTracks(colTracks, gridChildren, "width")
- Grid._measureAutoTracks(rowTracks, gridChildren, "height")
-
- Grid._resolveTracks(colTracks, availableWidth, columnGap)
- Grid._resolveTracks(rowTracks, availableHeight, rowGap)
-
- -- Compute column start positions (for positioning)
- local colStarts = {}
- local currentX = element.x + element.padding.left + reservedLeft
- for col = 1, #colTracks do
- colStarts[col] = currentX
- currentX = currentX + colTracks[col].value + columnGap
- end
-
- local rowStarts = {}
- local currentY = element.y + element.padding.top + reservedTop
- for row = 1, #rowTracks do
- rowStarts[row] = currentY
- currentY = currentY + rowTracks[row].value + rowGap
- end
-
- local effectiveAlignItems = element.alignItems or AlignItems.STRETCH
-
- for i, child in ipairs(gridChildren) do
- -- Calculate row and column (0-indexed for calculation)
- local index = i - 1
- local col = index % #colTracks
- local row = math.floor(index / #colTracks)
-
- if row >= #rowTracks then
- break
- end
-
- -- Get resolved cell position and size
- local colIdx = col + 1
- local rowIdx = row + 1
- local cellX = colStarts[colIdx]
- local cellY = rowStarts[rowIdx]
- local cellWidth = colTracks[colIdx].value
- local cellHeight = rowTracks[rowIdx].value
-
- -- Apply alignment within grid cell (default to stretch)
- -- BORDER-BOX MODEL: Set border-box dimensions, content area adjusts automatically
- if effectiveAlignItems == AlignItems.STRETCH or effectiveAlignItems == "stretch" then
- child.x = cellX
- child.y = cellY
- child._borderBoxWidth = cellWidth
- child._borderBoxHeight = cellHeight
- child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
- child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
- -- Disable auto-sizing when stretched by grid
- child.autosizing.width = false
- child.autosizing.height = false
- elseif effectiveAlignItems == AlignItems.CENTER or effectiveAlignItems == "center" then
- local childBorderBoxWidth = child:getBorderBoxWidth()
- local childBorderBoxHeight = child:getBorderBoxHeight()
- child.x = cellX + (cellWidth - childBorderBoxWidth) / 2
- child.y = cellY + (cellHeight - childBorderBoxHeight) / 2
- elseif
- effectiveAlignItems == AlignItems.FLEX_START
- or effectiveAlignItems == "flex-start"
- or effectiveAlignItems == "start"
- then
- child.x = cellX
- child.y = cellY
- elseif
- effectiveAlignItems == AlignItems.FLEX_END
- or effectiveAlignItems == "flex-end"
- or effectiveAlignItems == "end"
- then
- local childBorderBoxWidth = child:getBorderBoxWidth()
- local childBorderBoxHeight = child:getBorderBoxHeight()
- child.x = cellX + cellWidth - childBorderBoxWidth
- child.y = cellY + cellHeight - childBorderBoxHeight
- else
- child.x = cellX
- child.y = cellY
- child._borderBoxWidth = cellWidth
- child._borderBoxHeight = cellHeight
- child.width = math.max(0, cellWidth - child.padding.left - child.padding.right)
- child.height = math.max(0, cellHeight - child.padding.top - child.padding.bottom)
- -- Disable auto-sizing when stretched by grid
- child.autosizing.width = false
- child.autosizing.height = false
- end
-
- if #child.children > 0 then
- child:layoutChildren()
- end
- end
-end
-
-return Grid
diff --git a/libs/flexlove/modules/ImageCache.lua b/libs/flexlove/modules/ImageCache.lua
deleted file mode 100644
index 206fa59c..00000000
--- a/libs/flexlove/modules/ImageCache.lua
+++ /dev/null
@@ -1,160 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local function req(name)
- return require(modulePath .. name)
-end
-
-local utils = req("utils")
-
--- ErrorHandler will be injected via init
-local ErrorHandler = nil
-
----@class ImageCache
----@field _cache table
-local ImageCache = {}
-ImageCache._cache = {}
-
---- Initialize ImageCache with dependencies
----@param deps table Dependencies table with ErrorHandler
-function ImageCache.init(deps)
- if deps and deps.ErrorHandler then
- ErrorHandler = deps.ErrorHandler
- end
-end
-
---- Load an image from file path with caching
---- Returns cached image if already loaded, otherwise loads and caches it
----@param imagePath string -- Path to image file
----@param loadImageData boolean? -- Optional: also load ImageData for pixel access (default: false)
----@return love.Image|nil -- Image object or nil on error
----@return string|nil -- Error message if loading failed
-function ImageCache.load(imagePath, loadImageData)
- if not imagePath or type(imagePath) ~= "string" or imagePath == "" then
- return nil, "Invalid image path: path must be a non-empty string"
- end
-
- local normalizedPath = utils.normalizePath(imagePath)
-
- if ImageCache._cache[normalizedPath] then
- return ImageCache._cache[normalizedPath].image, nil
- end
-
- local success, imageOrError = pcall(love.graphics.newImage, normalizedPath)
- if not success then
- if ErrorHandler then
- ErrorHandler:warn("ImageCache", "RES_004", {
- resourceType = "image",
- path = imagePath,
- error = tostring(imageOrError),
- })
- end
- return nil, string.format("Failed to load image '%s': %s", imagePath, tostring(imageOrError))
- end
-
- local image = imageOrError
- local imgData = nil
-
- if loadImageData then
- local dataSuccess, dataOrError = pcall(love.image.newImageData, normalizedPath)
- if dataSuccess then
- imgData = dataOrError
- elseif ErrorHandler then
- ErrorHandler:warn("ImageCache", "RES_004", {
- resourceType = "image data",
- path = imagePath,
- error = tostring(dataOrError),
- })
- end
- end
-
- ImageCache._cache[normalizedPath] = {
- image = image,
- imageData = imgData,
- }
-
- return image, nil
-end
-
---- Get a cached image without loading
----@param imagePath string -- Path to image file
----@return love.Image|nil -- Cached image or nil if not found
-function ImageCache.get(imagePath)
- if not imagePath or type(imagePath) ~= "string" then
- return nil
- end
-
- local normalizedPath = utils.normalizePath(imagePath)
- local cached = ImageCache._cache[normalizedPath]
- return cached and cached.image or nil
-end
-
---- Get cached ImageData for an image
----@param imagePath string -- Path to image file
----@return love.ImageData|nil -- Cached ImageData or nil if not found
-function ImageCache.getImageData(imagePath)
- if not imagePath or type(imagePath) ~= "string" then
- return nil
- end
-
- local normalizedPath = utils.normalizePath(imagePath)
- local cached = ImageCache._cache[normalizedPath]
- return cached and cached.imageData or nil
-end
-
---- Remove a specific image from cache
----@param imagePath string -- Path to image file to remove
----@return boolean -- True if image was removed, false if not found
-function ImageCache.remove(imagePath)
- if not imagePath or type(imagePath) ~= "string" then
- return false
- end
-
- local normalizedPath = utils.normalizePath(imagePath)
- if ImageCache._cache[normalizedPath] then
- local cached = ImageCache._cache[normalizedPath]
- if cached.image then
- cached.image:release()
- end
- if cached.imageData then
- cached.imageData:release()
- end
- ImageCache._cache[normalizedPath] = nil
- return true
- end
- return false
-end
-
---- Clear all cached images
-function ImageCache.clear()
- for path, cached in pairs(ImageCache._cache) do
- if cached.image then
- cached.image:release()
- end
- if cached.imageData then
- cached.imageData:release()
- end
- end
- ImageCache._cache = {}
-end
-
---- Get cache statistics
----@return {count: number, memoryEstimate: number} -- Cache stats
-function ImageCache.getStats()
- local count = 0
- local memoryEstimate = 0
-
- for path, cached in pairs(ImageCache._cache) do
- count = count + 1
- if cached.image then
- local w, h = cached.image:getDimensions()
- -- Estimate: 4 bytes per pixel (RGBA)
- memoryEstimate = memoryEstimate + (w * h * 4)
- end
- end
-
- return {
- count = count,
- memoryEstimate = memoryEstimate,
- }
-end
-
-return ImageCache
diff --git a/libs/flexlove/modules/ImageRenderer.lua b/libs/flexlove/modules/ImageRenderer.lua
deleted file mode 100644
index 886c30a2..00000000
--- a/libs/flexlove/modules/ImageRenderer.lua
+++ /dev/null
@@ -1,380 +0,0 @@
----@class ImageRenderer
-local ImageRenderer = {}
-
--- ErrorHandler and utils will be injected via init
-local ErrorHandler = nil
-local utils = nil
-
---- Initialize ImageRenderer with dependencies
----@param deps table Dependencies table with ErrorHandler and utils
-function ImageRenderer.init(deps)
- if deps and deps.ErrorHandler then
- ErrorHandler = deps.ErrorHandler
- end
- if deps and deps.utils then
- utils = deps.utils
- end
-end
-
---- Calculate rendering parameters for object-fit modes
---- Returns source and destination rectangles for rendering
----@param imageWidth number -- Natural width of the image
----@param imageHeight number -- Natural height of the image
----@param boundsWidth number -- Width of the bounds to fit within
----@param boundsHeight number -- Height of the bounds to fit within
----@param fitMode string? -- One of: "fill", "contain", "cover", "scale-down", "none" (default: "fill")
----@param objectPosition string? -- Position like "center center", "top left", "50% 50%" (default: "center center")
----@return {sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, scaleX: number, scaleY: number}
-function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, fitMode, objectPosition)
- fitMode = fitMode or "fill"
- objectPosition = objectPosition or "center center"
-
- if imageWidth <= 0 or imageHeight <= 0 or boundsWidth <= 0 or boundsHeight <= 0 then
- ErrorHandler:error("ImageRenderer", "VAL_002", {
- imageWidth = imageWidth,
- imageHeight = imageHeight,
- boundsWidth = boundsWidth,
- boundsHeight = boundsHeight,
- })
- end
-
- local result = {
- sx = 0, -- Source X
- sy = 0, -- Source Y
- sw = imageWidth, -- Source width
- sh = imageHeight, -- Source height
- dx = 0, -- Destination X
- dy = 0, -- Destination Y
- dw = boundsWidth, -- Destination width
- dh = boundsHeight, -- Destination height
- scaleX = 1, -- Scale factor X
- scaleY = 1, -- Scale factor Y
- }
-
- if fitMode == "fill" then
- -- Stretch to fill bounds (may distort)
- result.scaleX = boundsWidth / imageWidth
- result.scaleY = boundsHeight / imageHeight
- result.dw = boundsWidth
- result.dh = boundsHeight
- elseif fitMode == "contain" then
- -- Scale to fit within bounds (preserves aspect ratio)
- local scale = math.min(boundsWidth / imageWidth, boundsHeight / imageHeight)
- result.scaleX = scale
- result.scaleY = scale
- result.dw = imageWidth * scale
- result.dh = imageHeight * scale
-
- -- Apply object-position for letterbox alignment
- local posX, posY = ImageRenderer._parsePosition(objectPosition)
- result.dx = (boundsWidth - result.dw) * posX
- result.dy = (boundsHeight - result.dh) * posY
- elseif fitMode == "cover" then
- -- Scale to cover bounds (preserves aspect ratio, may crop)
- local scale = math.max(boundsWidth / imageWidth, boundsHeight / imageHeight)
- result.scaleX = scale
- result.scaleY = scale
-
- local scaledWidth = imageWidth * scale
- local scaledHeight = imageHeight * scale
-
- -- Apply object-position for crop alignment
- local posX, posY = ImageRenderer._parsePosition(objectPosition)
-
- -- Calculate which part of the scaled image to show
- local cropX = (scaledWidth - boundsWidth) * posX
- local cropY = (scaledHeight - boundsHeight) * posY
-
- -- Convert back to source coordinates
- result.sx = cropX / scale
- result.sy = cropY / scale
- result.sw = boundsWidth / scale
- result.sh = boundsHeight / scale
-
- result.dx = 0
- result.dy = 0
- result.dw = boundsWidth
- result.dh = boundsHeight
- elseif fitMode == "none" then
- -- Use natural size (no scaling)
- result.scaleX = 1
- result.scaleY = 1
- result.dw = imageWidth
- result.dh = imageHeight
-
- -- Apply object-position
- local posX, posY = ImageRenderer._parsePosition(objectPosition)
- result.dx = (boundsWidth - imageWidth) * posX
- result.dy = (boundsHeight - imageHeight) * posY
- elseif fitMode == "scale-down" then
- -- Use none or contain, whichever is smaller
- if imageWidth <= boundsWidth and imageHeight <= boundsHeight then
- -- Image fits naturally, use "none"
- return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "none", objectPosition)
- else
- -- Image too large, use "contain"
- return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition)
- end
- else
- ErrorHandler:warn("ImageRenderer", "VAL_007", {
- fitMode = fitMode,
- fallback = "fill",
- })
- -- Use 'fill' as fallback
- return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "fill", objectPosition)
- end
-
- return result
-end
-
---- Parse object-position string into normalized coordinates (0-1)
---- Supports keywords (center, top, bottom, left, right) and percentages
----@param position string -- Position string like "center center", "top left", "50% 50%"
----@return number, number -- Normalized X and Y positions (0-1)
-function ImageRenderer._parsePosition(position)
- if not position or type(position) ~= "string" then
- return 0.5, 0.5 -- Default to center
- end
-
- -- Split into X and Y components
- local parts = {}
- for part in position:gmatch("%S+") do
- table.insert(parts, part:lower())
- end
-
- -- If only one value, use it for both axes (with special handling)
- if #parts == 1 then
- local val = parts[1]
- if val == "left" or val == "right" then
- parts = { val, "center" }
- elseif val == "top" or val == "bottom" then
- parts = { "center", val }
- else
- parts = { val, val }
- end
- elseif #parts == 0 then
- return 0.5, 0.5 -- Default to center
- end
-
- local function parseValue(val)
- -- Handle keywords
- if val == "center" then
- return 0.5
- elseif val == "left" or val == "top" then
- return 0
- elseif val == "right" or val == "bottom" then
- return 1
- end
-
- -- Handle percentages
- local percent = val:match("^([%d%.]+)%%$")
- if percent then
- return tonumber(percent) / 100
- end
-
- -- Handle plain numbers (treat as percentage)
- local num = tonumber(val)
- if num then
- return num / 100
- end
-
- -- Invalid value, default to center
- return 0.5
- end
-
- local x = parseValue(parts[1])
- local y = parseValue(parts[2] or parts[1])
-
- -- Clamp to 0-1 range
- x = math.max(0, math.min(1, x))
- y = math.max(0, math.min(1, y))
-
- return x, y
-end
-
---- Draw an image with specified object-fit mode
----@param image love.Image -- Image to draw
----@param x number -- X position of bounds
----@param y number -- Y position of bounds
----@param width number -- Width of bounds
----@param height number -- Height of bounds
----@param fitMode string? -- Object-fit mode (default: "fill")
----@param objectPosition string? -- Object-position (default: "center center")
----@param opacity number? -- Opacity 0-1 (default: 1)
----@param tintColor Color? -- Color to tint the image (default: white/no tint)
-function ImageRenderer.draw(image, x, y, width, height, fitMode, objectPosition, opacity, tintColor)
- if not image then
- return -- Nothing to draw
- end
-
- opacity = opacity or 1
- fitMode = fitMode or "fill"
- objectPosition = objectPosition or "center center"
-
- local imgWidth, imgHeight = image:getDimensions()
- local params = ImageRenderer.calculateFit(imgWidth, imgHeight, width, height, fitMode, objectPosition)
-
- -- Save current color
- local r, g, b, a = love.graphics.getColor()
-
- -- Apply opacity and tint
- if tintColor then
- love.graphics.setColor(tintColor.r, tintColor.g, tintColor.b, tintColor.a * opacity)
- else
- love.graphics.setColor(1, 1, 1, opacity)
- end
-
- -- Draw image
- if params.sx ~= 0 or params.sy ~= 0 or params.sw ~= imgWidth or params.sh ~= imgHeight then
- -- Need to use a quad for cropping
- local quad = love.graphics.newQuad(params.sx, params.sy, params.sw, params.sh, imgWidth, imgHeight)
- love.graphics.draw(image, quad, x + params.dx, y + params.dy, 0, params.dw / params.sw, params.dh / params.sh)
- else
- -- Simple draw with scaling
- love.graphics.draw(image, x + params.dx, y + params.dy, 0, params.scaleX, params.scaleY)
- end
-
- -- Restore color
- love.graphics.setColor(r, g, b, a)
-end
-
---- Draw an image with tiling/repeat mode
----@param image love.Image -- Image to draw
----@param x number -- X position of bounds
----@param y number -- Y position of bounds
----@param width number -- Width of bounds
----@param height number -- Height of bounds
----@param repeatMode string? -- Repeat mode: "repeat", "repeat-x", "repeat-y", "no-repeat", "space", "round" (default: "no-repeat")
----@param opacity number? -- Opacity 0-1 (default: 1)
----@param tintColor Color? -- Color to tint the image (default: white/no tint)
-function ImageRenderer.drawTiled(image, x, y, width, height, repeatMode, opacity, tintColor)
- if not image then
- return -- Nothing to draw
- end
-
- opacity = opacity or 1
- repeatMode = repeatMode or "no-repeat"
-
- local imgWidth, imgHeight = image:getDimensions()
-
- -- Save current color
- local r, g, b, a = love.graphics.getColor()
-
- -- Apply opacity and tint
- if tintColor then
- love.graphics.setColor(tintColor.r, tintColor.g, tintColor.b, tintColor.a * opacity)
- else
- love.graphics.setColor(1, 1, 1, opacity)
- end
-
- if repeatMode == "no-repeat" then
- -- Just draw once, no tiling
- love.graphics.draw(image, x, y)
- elseif repeatMode == "repeat" then
- -- Tile in both directions
- local tilesX = math.ceil(width / imgWidth)
- local tilesY = math.ceil(height / imgHeight)
-
- for tileY = 0, tilesY - 1 do
- for tileX = 0, tilesX - 1 do
- local drawX = x + (tileX * imgWidth)
- local drawY = y + (tileY * imgHeight)
-
- -- Calculate how much of the tile to draw (for partial tiles at edges)
- local drawWidth = math.min(imgWidth, width - (tileX * imgWidth))
- local drawHeight = math.min(imgHeight, height - (tileY * imgHeight))
-
- if drawWidth < imgWidth or drawHeight < imgHeight then
- -- Use quad for partial tile
- local quad = love.graphics.newQuad(0, 0, drawWidth, drawHeight, imgWidth, imgHeight)
- love.graphics.draw(image, quad, drawX, drawY)
- else
- -- Draw full tile
- love.graphics.draw(image, drawX, drawY)
- end
- end
- end
- elseif repeatMode == "repeat-x" then
- -- Tile horizontally only
- local tilesX = math.ceil(width / imgWidth)
-
- for tileX = 0, tilesX - 1 do
- local drawX = x + (tileX * imgWidth)
- local drawWidth = math.min(imgWidth, width - (tileX * imgWidth))
-
- if drawWidth < imgWidth then
- -- Use quad for partial tile
- local quad = love.graphics.newQuad(0, 0, drawWidth, imgHeight, imgWidth, imgHeight)
- love.graphics.draw(image, quad, drawX, y)
- else
- -- Draw full tile
- love.graphics.draw(image, drawX, y)
- end
- end
- elseif repeatMode == "repeat-y" then
- -- Tile vertically only
- local tilesY = math.ceil(height / imgHeight)
-
- for tileY = 0, tilesY - 1 do
- local drawY = y + (tileY * imgHeight)
- local drawHeight = math.min(imgHeight, height - (tileY * imgHeight))
-
- if drawHeight < imgHeight then
- -- Use quad for partial tile
- local quad = love.graphics.newQuad(0, 0, imgWidth, drawHeight, imgWidth, imgHeight)
- love.graphics.draw(image, quad, x, drawY)
- else
- -- Draw full tile
- love.graphics.draw(image, x, drawY)
- end
- end
- elseif repeatMode == "space" then
- -- Distribute tiles with even spacing
- local tilesX = math.floor(width / imgWidth)
- local tilesY = math.floor(height / imgHeight)
-
- if tilesX < 1 then
- tilesX = 1
- end
- if tilesY < 1 then
- tilesY = 1
- end
-
- local spaceX = tilesX > 1 and (width - (tilesX * imgWidth)) / (tilesX - 1) or 0
- local spaceY = tilesY > 1 and (height - (tilesY * imgHeight)) / (tilesY - 1) or 0
-
- for tileY = 0, tilesY - 1 do
- for tileX = 0, tilesX - 1 do
- local drawX = x + (tileX * (imgWidth + spaceX))
- local drawY = y + (tileY * (imgHeight + spaceY))
- love.graphics.draw(image, drawX, drawY)
- end
- end
- elseif repeatMode == "round" then
- -- Scale tiles to fit bounds exactly
- local tilesX = math.max(1, utils.round(width / imgWidth))
- local tilesY = math.max(1, utils.round(height / imgHeight))
-
- local scaleX = width / (tilesX * imgWidth)
- local scaleY = height / (tilesY * imgHeight)
-
- for tileY = 0, tilesY - 1 do
- for tileX = 0, tilesX - 1 do
- local drawX = x + (tileX * imgWidth * scaleX)
- local drawY = y + (tileY * imgHeight * scaleY)
- love.graphics.draw(image, drawX, drawY, 0, scaleX, scaleY)
- end
- end
- else
- ErrorHandler:warn("ImageRenderer", "VAL_007", {
- repeatMode = repeatMode,
- fallback = "no-repeat",
- })
- love.graphics.draw(image, x, y)
- end
-
- -- Restore color
- love.graphics.setColor(r, g, b, a)
-end
-
-return ImageRenderer
diff --git a/libs/flexlove/modules/ImageScaler.lua b/libs/flexlove/modules/ImageScaler.lua
deleted file mode 100644
index cf510bf3..00000000
--- a/libs/flexlove/modules/ImageScaler.lua
+++ /dev/null
@@ -1,174 +0,0 @@
--- ====================
--- ImageScaler
--- ====================
-
-local ImageScaler = {}
-
--- ErrorHandler will be injected via init
-local ErrorHandler = nil
-
---- Initialize ImageScaler with dependencies
----@param deps table Dependencies table with ErrorHandler
-function ImageScaler.init(deps)
- if deps and deps.ErrorHandler then
- ErrorHandler = deps.ErrorHandler
- end
-end
-
---- Scale an ImageData region using nearest-neighbor sampling
---- Produces sharp, pixelated scaling - ideal for pixel art
----@param sourceImageData love.ImageData -- Source image data
----@param srcX number -- Source region X (0-based)
----@param srcY number -- Source region Y (0-based)
----@param srcW number -- Source region width
----@param srcH number -- Source region height
----@param destW number -- Destination width
----@param destH number -- Destination height
----@return love.ImageData -- Scaled image data
-function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
- if not sourceImageData then
- ErrorHandler:error("ImageScaler", "VAL_001", {
- parameter = "sourceImageData",
- })
- end
-
- if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
- ErrorHandler:warn("ImageScaler", "VAL_002", {
- srcW = srcW,
- srcH = srcH,
- destW = destW,
- destH = destH,
- fallback = "1x1 transparent image",
- })
- -- Return a minimal 1x1 transparent image as fallback
- local fallbackImageData = love.image.newImageData(1, 1)
- fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
- return fallbackImageData
- end
-
- -- Create destination ImageData
- local destImageData = love.image.newImageData(destW, destH)
-
- -- Calculate scale ratios (cached outside loops for performance)
- local scaleX = srcW / destW
- local scaleY = srcH / destH
-
- -- Nearest-neighbor sampling
- for destY = 0, destH - 1 do
- for destX = 0, destW - 1 do
- -- Calculate source pixel coordinates using floor (nearest-neighbor)
- local srcPixelX = math.floor(destX * scaleX) + srcX
- local srcPixelY = math.floor(destY * scaleY) + srcY
-
- -- Clamp to source bounds (safety check)
- srcPixelX = math.min(srcPixelX, srcX + srcW - 1)
- srcPixelY = math.min(srcPixelY, srcY + srcH - 1)
-
- -- Sample source pixel
- local r, g, b, a = sourceImageData:getPixel(srcPixelX, srcPixelY)
-
- -- Write to destination
- destImageData:setPixel(destX, destY, r, g, b, a)
- end
- end
-
- return destImageData
-end
-
---- Linear interpolation helper
---- Blends between two values based on interpolation factor
----@param a number -- Start value
----@param b number -- End value
----@param t number -- Interpolation factor [0, 1]
----@return number -- Interpolated value
-local function lerp(a, b, t)
- return a + (b - a) * t
-end
-
---- Scale an ImageData region using bilinear interpolation
---- Produces smooth, filtered scaling - ideal for high-quality upscaling
----@param sourceImageData love.ImageData -- Source image data
----@param srcX number -- Source region X (0-based)
----@param srcY number -- Source region Y (0-based)
----@param srcW number -- Source region width
----@param srcH number -- Source region height
----@param destW number -- Destination width
----@param destH number -- Destination height
----@return love.ImageData -- Scaled image data
-function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
- if not sourceImageData then
- ErrorHandler:error("ImageScaler", "VAL_001", {
- parameter = "sourceImageData",
- })
- end
-
- if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
- ErrorHandler:warn("ImageScaler", "VAL_002", {
- srcW = srcW,
- srcH = srcH,
- destW = destW,
- destH = destH,
- fallback = "1x1 transparent image",
- })
- -- Return a minimal 1x1 transparent image as fallback
- local fallbackImageData = love.image.newImageData(1, 1)
- fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
- return fallbackImageData
- end
-
- -- Create destination ImageData
- local destImageData = love.image.newImageData(destW, destH)
-
- -- Calculate scale ratios
- local scaleX = srcW / destW
- local scaleY = srcH / destH
-
- -- Bilinear interpolation
- for destY = 0, destH - 1 do
- for destX = 0, destW - 1 do
- -- Calculate fractional source position
- local srcXf = destX * scaleX
- local srcYf = destY * scaleY
-
- -- Get integer coordinates for 2x2 sampling grid
- local x0 = math.floor(srcXf)
- local y0 = math.floor(srcYf)
- local x1 = math.min(x0 + 1, srcW - 1)
- local y1 = math.min(y0 + 1, srcH - 1)
-
- -- Get fractional parts for interpolation
- local fx = srcXf - x0
- local fy = srcYf - y0
-
- -- Sample 4 neighboring pixels (with source offset)
- local r00, g00, b00, a00 = sourceImageData:getPixel(srcX + x0, srcY + y0)
- local r10, g10, b10, a10 = sourceImageData:getPixel(srcX + x1, srcY + y0)
- local r01, g01, b01, a01 = sourceImageData:getPixel(srcX + x0, srcY + y1)
- local r11, g11, b11, a11 = sourceImageData:getPixel(srcX + x1, srcY + y1)
-
- -- Interpolate horizontally (top and bottom rows)
- local rTop = lerp(r00, r10, fx)
- local gTop = lerp(g00, g10, fx)
- local bTop = lerp(b00, b10, fx)
- local aTop = lerp(a00, a10, fx)
-
- local rBottom = lerp(r01, r11, fx)
- local gBottom = lerp(g01, g11, fx)
- local bBottom = lerp(b01, b11, fx)
- local aBottom = lerp(a01, a11, fx)
-
- -- Interpolate vertically (final result)
- local r = lerp(rTop, rBottom, fy)
- local g = lerp(gTop, gBottom, fy)
- local b = lerp(bTop, bBottom, fy)
- local a = lerp(aTop, aBottom, fy)
-
- -- Write to destination
- destImageData:setPixel(destX, destY, r, g, b, a)
- end
- end
-
- return destImageData
-end
-
-return ImageScaler
diff --git a/libs/flexlove/modules/InputEvent.lua b/libs/flexlove/modules/InputEvent.lua
deleted file mode 100644
index 8f1be533..00000000
--- a/libs/flexlove/modules/InputEvent.lua
+++ /dev/null
@@ -1,88 +0,0 @@
----@class InputEvent
----@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"hover"|"unhover"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel"
----@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle)
----@field x number -- Mouse/Touch X position
----@field y number -- Mouse/Touch Y position
----@field dx number? -- Delta X from drag/touch start (only for drag/touch events)
----@field dy number? -- Delta Y from drag/touch start (only for drag/touch events)
----@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
----@field clickCount number -- Number of clicks (for double/triple click detection)
----@field timestamp number -- Time when event occurred
----@field touchId string? -- Touch identifier (for multi-touch)
----@field pressure number? -- Touch pressure (0-1, defaults to 1.0)
----@field phase string? -- Touch phase: "began", "moved", "ended", "cancelled"
-local InputEvent = {}
-InputEvent.__index = InputEvent
-
----@class InputEventProps
----@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"hover"|"unhover"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel"
----@field button number
----@field x number
----@field y number
----@field dx number?
----@field dy number?
----@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
----@field clickCount number?
----@field timestamp number?
----@field touchId string?
----@field pressure number?
----@field phase string?
-
---- Create a new input event
----@param props InputEventProps
----@return InputEvent
-function InputEvent.new(props)
- local self = setmetatable({}, InputEvent)
- self.type = props.type
- self.button = props.button
- self.x = props.x
- self.y = props.y
- self.dx = props.dx
- self.dy = props.dy
- self.modifiers = props.modifiers
- self.clickCount = props.clickCount or 1
- self.timestamp = props.timestamp or love.timer.getTime()
-
- -- Touch-specific properties
- self.touchId = props.touchId
- self.pressure = props.pressure or 1.0
- self.phase = props.phase
-
- return self
-end
-
---- Create an InputEvent from LÖVE touch data
----@param id userdata Touch ID from LÖVE
----@param x number Touch X position
----@param y number Touch Y position
----@param phase string Touch phase: "began", "moved", "ended", "cancelled"
----@param pressure number? Touch pressure (0-1, defaults to 1.0)
----@return InputEvent
-function InputEvent.fromTouch(id, x, y, phase, pressure)
- local touchIdStr = tostring(id)
- local eventType = "touchpress"
- if phase == "moved" then
- eventType = "touchmove"
- elseif phase == "ended" then
- eventType = "touchrelease"
- elseif phase == "cancelled" then
- eventType = "touchcancel"
- end
-
- return InputEvent.new({
- type = eventType,
- button = 1, -- Treat touch as left button
- x = x,
- y = y,
- dx = 0,
- dy = 0,
- modifiers = { shift = false, ctrl = false, alt = false, super = false },
- clickCount = 1,
- timestamp = love.timer.getTime(),
- touchId = touchIdStr,
- pressure = pressure or 1.0,
- phase = phase,
- })
-end
-
-return InputEvent
diff --git a/libs/flexlove/modules/KeyboardNavigation.lua b/libs/flexlove/modules/KeyboardNavigation.lua
deleted file mode 100644
index 7122cd40..00000000
--- a/libs/flexlove/modules/KeyboardNavigation.lua
+++ /dev/null
@@ -1,748 +0,0 @@
-local packageName = ... or "KeyboardNavigation"
-local modulePath = packageName:match("(.-)[^%.]+$")
-
-local function req(name)
- return require(modulePath .. name)
-end
-
----@class KeyboardNavigation
----@field config KeyboardNavigationConfig
-local KeyboardNavigation = {
-
- config = {
- -- Global settings
- enabled = true,
- debugMode = false,
-
- -- Key bindings
- keys = {
- next = "tab",
- previous = "shifttab",
- up = "up",
- down = "down",
- left = "left",
- right = "right",
- activate = { "return", "space" },
- dismiss = "escape",
- toggleDebug = "f12",
- inspect = "i",
- },
-
- -- Navigation behavior
- wrapAround = true,
- directionalNavigation = true,
- focusVisible = true,
- autofocusOnCreate = false,
-
- --- Drop focus after pressing Enter/Space to activate an element
- --- When false, focus remains on the element after activation
- dropFocusOnSelection = true,
-
- -- Developer tools
- developerTools = {
- enabled = true,
- showProperties = true,
- highlightColor = { 1, 0.8, 0, 0.5 },
- },
-
- -- Focus indicator style
- focusIndicator = {
- color = { 0.2, 0.6, 1.0, 0.8 },
- lineWidth = 2,
- inset = -3,
- borderRadius = 4,
- animationDuration = 0.15,
- },
- },
-
- -- State
- _navigationStack = {},
- _lastNavigationTime = 0,
- _inspectMode = false,
- _deps = nil,
-
- -- Spatial index for directional navigation (performance optimization)
- _spatialIndex = {
- enabled = false,
- cellSize = 100, -- Grid cell size in pixels
- grid = {}, -- Grid storing element references
- elementPositions = {}, -- Cache of element positions {element = {x, y, w, h}}
- lastUpdateFrame = 0,
- },
-}
-
---- Initialize KeyboardNavigation module
----@param deps table {Context, Element, ErrorHandler, utils, InputEvent}
-function KeyboardNavigation.init(deps)
- -- Validate required dependencies
- local required = { Context = true, Element = true, ErrorHandler = true, utils = true, InputEvent = true }
- for depName, _ in pairs(required) do
- if not deps[depName] then
- error(string.format("KeyboardNavigation.init: Missing required dependency: %s", depName))
- end
- end
-
- KeyboardNavigation._deps = deps
- KeyboardNavigation._ErrorHandler = deps.ErrorHandler
- KeyboardNavigation._InputEvent = deps.InputEvent
- KeyboardNavigation._Context = deps.Context
- KeyboardNavigation._Element = deps.Element
- KeyboardNavigation._utils = deps.utils
-end
-
---- Handle keyboard press for navigation
----@param key string
----@param scancode string
----@param isrepeat boolean
----@return boolean handled
-function KeyboardNavigation:handleKeyPress(key, scancode, isrepeat)
- if not KeyboardNavigation._Context then
- return false
- end
-
- -- Debug logging
- if KeyboardNavigation.config.debugMode then
- print(
- string.format(
- "[KeyboardNavigation] Key pressed: %s (scancode: %s, repeat: %s)",
- key,
- scancode,
- tostring(isrepeat)
- )
- )
- print(string.format("[KeyboardNavigation] Enabled: %s", tostring(KeyboardNavigation.config.enabled)))
- end
-
- local config = KeyboardNavigation.config
- local keys = config.keys
-
- -- Check for activation keys
- for _, activateKey in ipairs(keys.activate) do
- if key == activateKey then
- return self:activateElement()
- end
- end
-
- -- Check for dismiss key
- if key == keys.dismiss then
- return self:dismissElement()
- end
-
- -- Check for next/previous navigation
- -- Tab with shift held = previous; Tab without shift = next
- if key == keys.next then
- if love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift") then
- return self:previousFocusable()
- end
- return self:nextFocusable()
- end
-
- if key == keys.previous then
- return self:previousFocusable()
- end
-
- -- Check for directional navigation
- if config.directionalNavigation then
- if key == keys.up then
- return self:navigateDirectional("up")
- elseif key == keys.down then
- return self:navigateDirectional("down")
- elseif key == keys.left then
- return self:navigateDirectional("left")
- elseif key == keys.right then
- return self:navigateDirectional("right")
- end
- end
-
- return false
-end
-
---- Find next focusable element in the focusable list
----@param focusableList table List of focusable elements in tab order
----@param current Element? Currently focused element
----@return Element?
-function KeyboardNavigation:_findNextInList(focusableList, current)
- local currentIndex = 0
- if current then
- for i, elem in ipairs(focusableList) do
- if elem.id == current.id then
- currentIndex = i
- break
- end
- end
- end
-
- -- Search forward
- if currentIndex < #focusableList then
- return focusableList[currentIndex + 1]
- end
-
- -- Wrap around if enabled
- if KeyboardNavigation.config.wrapAround and #focusableList > 0 then
- return focusableList[1]
- end
-
- return nil
-end
-
---- Get the focusable element list scoped to the navigation container
----@return Element[]
-function KeyboardNavigation:_getScopedFocusableList()
- local Context = KeyboardNavigation._Context
- local container = Context.getNavigationContainer()
- if container then
- return container:getFocusableChildren()
- end
- return Context.getFocusableElements()
-end
-
---- Navigate to next focusable element (Tab)
----@return boolean success
-function KeyboardNavigation:nextFocusable()
- local Context = KeyboardNavigation._Context
-
- local current = Context.getFocused()
- if KeyboardNavigation.config.debugMode then
- print(
- string.format("[KeyboardNavigation] Tab pressed - Current focus: %s", tostring(current and current.id or "nil"))
- )
- end
-
- local focusableList = self:_getScopedFocusableList()
- local nextElem = self:_findNextInList(focusableList, current)
-
- if nextElem then
- self:_focusElement(nextElem)
- return true
- end
-
- return false
-end
-
---- Find previous focusable element in the focusable list
----@param focusableList table List of focusable elements in tab order
----@param current Element? Currently focused element
----@return Element?
-function KeyboardNavigation:_findPreviousInList(focusableList, current)
- local currentIndex = #focusableList + 1
- if current then
- for i, elem in ipairs(focusableList) do
- if elem.id == current.id then
- currentIndex = i
- break
- end
- end
- end
-
- -- Search backward
- if currentIndex - 1 >= 1 then
- return focusableList[currentIndex - 1]
- end
-
- -- Wrap around if enabled
- if KeyboardNavigation.config.wrapAround and #focusableList > 0 then
- return focusableList[#focusableList]
- end
-
- return nil
-end
-
---- Navigate to previous focusable element (Shift+Tab)
----@return boolean success
-function KeyboardNavigation:previousFocusable()
- local Context = KeyboardNavigation._Context
-
- local current = Context.getFocused()
-
- local focusableList = self:_getScopedFocusableList()
- local prevElem = self:_findPreviousInList(focusableList, current)
-
- if prevElem then
- self:_focusElement(prevElem)
- return true
- end
-
- return false
-end
-
---- Navigate using arrow keys
----@param direction "up"|"down"|"left"|"right"
----@return boolean success
-function KeyboardNavigation:navigateDirectional(direction)
- local Context = KeyboardNavigation._Context
- local current = Context.getFocused()
-
- if not current then
- return false
- end
-
- local nextElem = KeyboardNavigation:_findDirectionalNeighbor(current, direction)
-
- if nextElem then
- self:_focusElement(nextElem)
- return true
- end
-
- return false
-end
-
---- Find closest focusable element in the given direction
----@param current Element
----@param direction "up"|"down"|"left"|"right"
----@return Element?
-function KeyboardNavigation:_findDirectionalNeighbor(current, direction)
- -- Try spatial index first if enabled
- if KeyboardNavigation._spatialIndex.enabled then
- local spatialResult = self:_findDirectionalNeighborSpatial(current, direction)
- if spatialResult then
- return spatialResult
- end
- end
-
- -- Collect all focusable elements visible this frame
- local Context = KeyboardNavigation._Context
- local focusable = {}
-
- local function collectFocusable(elem)
- if elem:isFocusable() and elem ~= current then
- table.insert(focusable, elem)
- end
- for _, child in ipairs(elem.children) do
- collectFocusable(child)
- end
- end
-
- -- Mode-agnostic: collect from Context's focusable list
- local allFocusable = Context.getFocusableElements()
- for _, elem in ipairs(allFocusable) do
- if elem ~= current then
- table.insert(focusable, elem)
- end
- end
-
- if #focusable == 0 then
- return nil
- end
-
- local currentRect = {
- x = current.x,
- y = current.y,
- width = current.width or 0,
- height = current.height or 0,
- }
-
- local closest = nil
- local closestDistance = math.huge
-
- for _, elem in ipairs(focusable) do
- local elemRect = {
- x = elem.x,
- y = elem.y,
- width = elem.width or 0,
- height = elem.height or 0,
- }
-
- local distance, isInDirection = self:_calculateDirectionalDistance(currentRect, elemRect, direction)
-
- if isInDirection and distance < closestDistance then
- closest = elem
- closestDistance = distance
- end
- end
-
- -- If no element found in exact direction, try with looser criteria
- if not closest then
- closest = self:_findClosestInDirection(current, focusable, direction)
- end
-
- return closest
-end
-
---- Calculate distance and direction between elements
----@param from table {x, y, width, height}
----@param to table {x, y, width, height}
----@param direction string
----@return number distance, boolean isInDirection
-function KeyboardNavigation:_calculateDirectionalDistance(from, to, direction)
- -- Calculate bounding box edges
- local fromLeft = from.x
- local fromRight = from.x + from.width
- local fromTop = from.y
- local fromBottom = from.y + from.height
-
- local toLeft = to.x
- local toRight = to.x + to.width
- local toTop = to.y
- local toBottom = to.y + to.height
-
- local distance = math.huge
- local isInDirection = false
-
- if direction == "up" then
- if toBottom < fromTop then
- isInDirection = true
- distance = fromTop - toBottom
- end
- elseif direction == "down" then
- if toTop > fromBottom then
- isInDirection = true
- distance = toTop - fromBottom
- end
- elseif direction == "left" then
- if toRight < fromLeft then
- isInDirection = true
- distance = fromLeft - toRight
- end
- elseif direction == "right" then
- if toLeft > fromRight then
- isInDirection = true
- distance = toLeft - fromRight
- end
- end
-
- return distance, isInDirection
-end
-
---- Find closest element in direction using center-to-center distance
----@param current Element
----@param focusable Element[]
----@param direction string
----@return Element?
-function KeyboardNavigation:_findClosestInDirection(current, focusable, direction)
- local currentCenterX = current.x + (current.width or 0) / 2
- local currentCenterY = current.y + (current.height or 0) / 2
-
- local closest = nil
- local closestDistance = math.huge
-
- for _, elem in ipairs(focusable) do
- if elem ~= current then
- local elemCenterX = elem.x + (elem.width or 0) / 2
- local elemCenterY = elem.y + (elem.height or 0) / 2
-
- local dx = elemCenterX - currentCenterX
- local dy = elemCenterY - currentCenterY
-
- -- Check if element is generally in the right direction
- local isInDirection = false
-
- if direction == "up" and dy < 0 then
- isInDirection = true
- elseif direction == "down" and dy > 0 then
- isInDirection = true
- elseif direction == "left" and dx < 0 then
- isInDirection = true
- elseif direction == "right" and dx > 0 then
- isInDirection = true
- end
-
- if isInDirection then
- local distance = math.sqrt(dx * dx + dy * dy)
- if distance < closestDistance then
- closest = elem
- closestDistance = distance
- end
- end
- end
- end
-
- return closest
-end
-
---- Focus an element
----@param element Element
-function KeyboardNavigation:_focusElement(element)
- local Context = KeyboardNavigation._Context
-
- if element and element:isFocusable() then
- if KeyboardNavigation.config.debugMode then
- print(
- string.format(
- "[KeyboardNavigation] Focusing element: %s (id: %s)",
- element.themeComponent or "unknown",
- tostring(element.id)
- )
- )
- end
- Context.setFocused(element)
-
- -- Update focus indicator
- if KeyboardNavigation.FocusIndicator then
- KeyboardNavigation.FocusIndicator.setFocused(element)
- end
-
- -- Call onFocus callback if it exists
- if element.onFocus then
- local success, err = pcall(function()
- if element.onFocusDeferred then
- table.insert(Context._deferredCallbacks or {}, function()
- element:onFocus(element)
- end)
- else
- element:onFocus(element)
- end
- end)
-
- if not success then
- KeyboardNavigation._ErrorHandler:warn("KeyboardNavigation", "NAV_001", {
- elementId = element.id or "unknown",
- error = tostring(err),
- })
- end
- end
- end
-end
-
----@param element Element
----@return boolean
-function KeyboardNavigation:_shouldDropFocusOnSelection(element)
- if element and element.dropFocusOnSelection ~= nil then
- return element.dropFocusOnSelection == true
- end
-
- return KeyboardNavigation.config.dropFocusOnSelection == true
-end
-
---- Activate currently focused element
----@return boolean success
-function KeyboardNavigation:activateElement()
- local Context = KeyboardNavigation._Context
- local focused = Context.getFocused()
-
- if not focused then
- return false
- end
-
- if focused.disabled then
- return false
- end
-
- -- Fire press and release events
- if focused.onEvent then
- local modifiers = KeyboardNavigation._utils.getModifiers()
- local pressEvent = KeyboardNavigation._InputEvent.new({
- type = "press",
- button = 1,
- x = focused.x,
- y = focused.y,
- modifiers = modifiers,
- clickCount = 1,
- })
-
- local releaseEvent = KeyboardNavigation._InputEvent.new({
- type = "release",
- button = 1,
- x = focused.x,
- y = focused.y,
- modifiers = modifiers,
- clickCount = 1,
- })
-
- local success, err = pcall(function()
- focused.onEvent(focused, pressEvent)
- focused.onEvent(focused, releaseEvent)
- end)
-
- if not success then
- KeyboardNavigation._ErrorHandler:warn("KeyboardNavigation", "NAV_002", {
- elementId = focused.id or "unknown",
- error = tostring(err),
- })
- end
-
- -- Drop focus after selection based on per-element override or global config.
- if KeyboardNavigation:_shouldDropFocusOnSelection(focused) then
- Context.clearFocus()
- if KeyboardNavigation.FocusIndicator then
- KeyboardNavigation.FocusIndicator.setFocused(nil)
- end
- end
-
- return true
- end
-
- return false
-end
-
---- Dismiss currently focused element
----@return boolean success
-function KeyboardNavigation:dismissElement()
- local Context = KeyboardNavigation._Context
- local focused = Context.getFocused()
-
- if not focused then
- return false
- end
-
- -- Check if element has a dismiss handler
- if focused.onDismiss then
- local success, err = pcall(function()
- if focused.onDismissDeferred then
- table.insert(Context._deferredCallbacks or {}, function()
- focused:onDismiss(focused)
- end)
- else
- focused:onDismiss(focused)
- end
- end)
-
- if not success then
- KeyboardNavigation._ErrorHandler:warn("KeyboardNavigation", "NAV_003", {
- elementId = focused.id or "unknown",
- error = tostring(err),
- })
- end
-
- return true -- Handler took care of dismissal
- end
-
- -- Default behavior: blur the element (only if no onDismiss handler)
- Context.clearFocus()
- return true
-end
-
---- Update keyboard navigation (for animations, etc.)
----@param dt number
-function KeyboardNavigation:update(dt)
- -- Update focus indicator if it exists
- if KeyboardNavigation.FocusIndicator then
- KeyboardNavigation.FocusIndicator:update(dt)
- end
-end
-
---- Push current focus onto stack (for modals/dialogs)
---- Saves current focus and sets new focus to the given element
----@param element Element? The element to focus (e.g., modal dialog)
-function KeyboardNavigation:pushFocus(element)
- local Context = KeyboardNavigation._Context
-
- table.insert(KeyboardNavigation._navigationStack, Context.getFocused())
- Context.pushFocusStack(element)
-end
-
---- Pop focus from stack (return from modal)
---- Restores previously focused element from the stack
----@return Element? The previously focused element, or nil if stack was empty
-function KeyboardNavigation:popFocus()
- local Context = KeyboardNavigation._Context
-
- local previous = Context.popFocusStack()
- if #KeyboardNavigation._navigationStack > 0 then
- previous = table.remove(KeyboardNavigation._navigationStack)
- end
-
- return previous
-end
-
--- ====================
--- Spatial Index (Performance Optimization)
--- ====================
-
---- Enable spatial index for faster directional navigation
----@param enabled boolean
-function KeyboardNavigation.enableSpatialIndex(enabled)
- KeyboardNavigation._spatialIndex.enabled = enabled
- if not enabled then
- KeyboardNavigation:_clearSpatialIndex()
- end
-end
-
---- Clear spatial index
-function KeyboardNavigation:_clearSpatialIndex()
- KeyboardNavigation._spatialIndex.grid = {}
- KeyboardNavigation._spatialIndex.elementPositions = {}
-end
-
---- Find directional neighbor using spatial index
----@param current Element
----@param direction "up"|"down"|"left"|"right"
----@return Element?
-function KeyboardNavigation:_findDirectionalNeighborSpatial(current, direction)
- local index = KeyboardNavigation._spatialIndex
- local cellSize = index.cellSize
-
- -- Get current element's grid position
- local currentPos = index.elementPositions[current]
- if not currentPos then
- return nil
- end
-
- local centerX = currentPos.x + currentPos.w / 2
- local centerY = currentPos.y + currentPos.h / 2
- local currentCellX = math.floor(centerX / cellSize)
- local currentCellY = math.floor(centerY / cellSize)
-
- -- Search in direction, expanding outward
- local maxSearchRadius = 20 -- Maximum cells to search
- local visited = {}
-
- for radius = 1, maxSearchRadius do
- local candidates = {}
-
- -- Get cells in the search ring
- if direction == "up" then
- table.insert(candidates, { currentCellX, currentCellY - radius })
- if radius > 1 then
- table.insert(candidates, { currentCellX - 1, currentCellY - radius })
- table.insert(candidates, { currentCellX + 1, currentCellY - radius })
- end
- elseif direction == "down" then
- table.insert(candidates, { currentCellX, currentCellY + radius })
- if radius > 1 then
- table.insert(candidates, { currentCellX - 1, currentCellY + radius })
- table.insert(candidates, { currentCellX + 1, currentCellY + radius })
- end
- elseif direction == "left" then
- table.insert(candidates, { currentCellX - radius, currentCellY })
- if radius > 1 then
- table.insert(candidates, { currentCellX - radius, currentCellY - 1 })
- table.insert(candidates, { currentCellX - radius, currentCellY + 1 })
- end
- elseif direction == "right" then
- table.insert(candidates, { currentCellX + radius, currentCellY })
- if radius > 1 then
- table.insert(candidates, { currentCellX + radius, currentCellY - 1 })
- table.insert(candidates, { currentCellX + radius, currentCellY + 1 })
- end
- end
-
- -- Check each candidate cell
- for _, cell in ipairs(candidates) do
- local cellKey = string.format("%d,%d", cell[1], cell[2])
- local cellElements = index.grid[cellKey]
-
- if cellElements then
- for _, elem in ipairs(cellElements) do
- if elem ~= current and not visited[elem] then
- visited[elem] = true
- local elemPos = index.elementPositions[elem]
- if elemPos then
- local elemCenterX = elemPos.x + elemPos.w / 2
- local elemCenterY = elemPos.y + elemPos.h / 2
-
- -- Check if element is in the correct direction
- local isInDirection = false
- if direction == "up" and elemCenterY < centerY then
- isInDirection = true
- elseif direction == "down" and elemCenterY > centerY then
- isInDirection = true
- elseif direction == "left" and elemCenterX < centerX then
- isInDirection = true
- elseif direction == "right" and elemCenterX > centerX then
- isInDirection = true
- end
-
- if isInDirection then
- return elem
- end
- end
- end
- end
- end
- end
- end
-
- return nil
-end
-
-return KeyboardNavigation
diff --git a/libs/flexlove/modules/LayoutEngine.lua b/libs/flexlove/modules/LayoutEngine.lua
deleted file mode 100644
index cf4dec4d..00000000
--- a/libs/flexlove/modules/LayoutEngine.lua
+++ /dev/null
@@ -1,1714 +0,0 @@
----@class LayoutEngine
----@field element Element? Reference to the parent element
----@field positioning Positioning Layout positioning mode
----@field flexDirection FlexDirection Direction of flex layout
----@field justifyContent JustifyContent Alignment of items along main axis
----@field alignItems AlignItems Alignment of items along cross axis
----@field alignContent AlignContent Alignment of lines in multi-line flex containers
----@field flexWrap FlexWrap Whether children wrap to multiple lines
----@field gap number Space between children elements
----@field gridRows number? Number of rows in the grid
----@field gridColumns number? Number of columns in the grid
----@field columnGap number? Gap between grid columns
----@field rowGap number? Gap between grid rows
----@field _Grid table
----@field _Units table
----@field _Context table
----@field _Positioning table
----@field _FlexDirection table
----@field _JustifyContent table
----@field _AlignContent table
----@field _AlignItems table
----@field _AlignSelf table
----@field _FlexWrap table
----@field _layoutCount number Track layout recalculations per frame
----@field _lastFrameCount number Last frame number for resetting counters
----@field _ErrorHandler ErrorHandler? ErrorHandler module dependency
----@field _Performance Performance? Performance module dependency
-local LayoutEngine = {}
-LayoutEngine.__index = LayoutEngine
-
---- Recursively shift an element and all its descendants by (dx, dy).
---- Used by the row-reverse mirror pass and the `position: relative` offset
---- pass: both run after the rest of layout has placed the subtree, so a single
---- delta walk keeps descendants visually anchored to the parent.
----@param elem Element
----@param dx number
----@param dy number
-local function shiftSubtree(elem, dx, dy)
- elem.x = elem.x + dx
- elem.y = elem.y + dy
- for _, c in ipairs(elem.children) do
- shiftSubtree(c, dx, dy)
- end
-end
-
---- Initialize module with shared dependencies
----@param deps table Dependencies {ErrorHandler, Performance, utils}
-function LayoutEngine.init(deps)
- LayoutEngine._ErrorHandler = deps.ErrorHandler
- LayoutEngine._Performance = deps.Performance
- LayoutEngine._Utils = deps.utils
-end
-
----@class LayoutEngineProps
----@field positioning Positioning? Layout positioning mode (default: RELATIVE)
----@field flexDirection FlexDirection? Direction of flex layout (default: HORIZONTAL)
----@field justifyContent JustifyContent? Alignment of items along main axis (default: FLEX_START)
----@field alignItems AlignItems? Alignment of items along cross axis (default: STRETCH)
----@field alignContent AlignContent? Alignment of lines in multi-line flex containers (default: STRETCH)
----@field flexWrap FlexWrap? Whether children wrap to multiple lines (default: NOWRAP)
----@field gap number? Space between children elements (default: 10)
----@field gridRows number? Number of rows in the grid
----@field gridColumns number? Number of columns in the grid
----@field columnGap number? Gap between grid columns
----@field rowGap number? Gap between grid rows
-
---- Create a new LayoutEngine instance
----@param props LayoutEngineProps
----@param deps table Dependencies {utils, Grid, Units, Context}
----@return LayoutEngine
-function LayoutEngine.new(props, deps)
- local enums = deps.utils.enums
- local Positioning = enums.Positioning
- local FlexDirection = enums.FlexDirection
- local JustifyContent = enums.JustifyContent
- local AlignContent = enums.AlignContent
- local AlignItems = enums.AlignItems
- local AlignSelf = enums.AlignSelf
- local FlexWrap = enums.FlexWrap
-
- local self = setmetatable({}, LayoutEngine)
-
- -- Store dependencies for instance methods
- self._Grid = deps.Grid
- self._Units = deps.Units
- self._Context = deps.Context
- self._ErrorHandler = deps.ErrorHandler
- self._Positioning = Positioning
- self._FlexDirection = FlexDirection
- self._JustifyContent = JustifyContent
- self._AlignContent = AlignContent
- self._AlignItems = AlignItems
- self._AlignSelf = AlignSelf
- self._FlexWrap = FlexWrap
-
- -- Layout configuration
- self.positioning = props.positioning or Positioning.FLEX
- self.flexDirection = props.flexDirection or FlexDirection.HORIZONTAL
- self.justifyContent = props.justifyContent or JustifyContent.FLEX_START
- self.alignItems = props.alignItems or AlignItems.STRETCH
- self.alignContent = props.alignContent or AlignContent.STRETCH
- self.flexWrap = props.flexWrap or FlexWrap.NOWRAP
- self.gap = props.gap or 10
-
- -- Grid layout configuration
- self.gridRows = props.gridRows
- self.gridColumns = props.gridColumns
-
- self.columnGap = props.columnGap
- self.rowGap = props.rowGap
-
- -- Element reference (will be set via initialize)
- self.element = nil
-
- -- Performance tracking
- self._layoutCount = 0
- self._lastFrameCount = 0
-
- -- Layout memoization cache
- self._layoutCache = {
- childrenCount = 0,
- containerWidth = 0,
- containerHeight = 0,
- containerX = 0,
- containerY = 0,
- childrenHash = "",
- }
-
- return self
-end
-
---- Initialize the LayoutEngine with its parent element
----@param element Element The parent element
-function LayoutEngine:initialize(element)
- self.element = element
-end
-
---- True for flex-direction `horizontal` or `horizontal-reverse` (and their
---- `row`/`row-reverse` aliases, which normalize to those at construction).
---- Routes every main-axis orientation check so reverse directions are
---- correctly classified as horizontal.
----@return boolean
-function LayoutEngine:_isHorizontal()
- return self.flexDirection == self._FlexDirection.HORIZONTAL
- or self.flexDirection == self._FlexDirection.HORIZONTAL_REVERSE
-end
-
---- True for flex-direction `horizontal-reverse` or `vertical-reverse`
---- (and their `row-reverse`/`column-reverse` aliases).
----@return boolean
-function LayoutEngine:_isReverse()
- return self.flexDirection == self._FlexDirection.HORIZONTAL_REVERSE
- or self.flexDirection == self._FlexDirection.VERTICAL_REVERSE
-end
-
---- Apply CSS positioning offsets (top, right, bottom, left) to a child element
----@param child Element The element to apply offsets to
-function LayoutEngine:applyPositioningOffsets(child)
- if not child then
- return
- end
-
- -- For CSS-style positioning, we need the parent's bounds
- local parent = child.parent
- if not parent then
- return
- end
-
- -- Only apply offsets to explicitly absolute children or children in relative/absolute containers
- -- Flex/grid children ignore positioning offsets as they participate in layout
- local isFlexChild = child.positioning == self._Positioning.FLEX
- or child.positioning == self._Positioning.GRID
- or (child.positioning == self._Positioning.ABSOLUTE and not child._explicitlyAbsolute)
-
- if not isFlexChild and child._explicitlyAbsolute then
- -- Apply absolute positioning for explicitly absolute children
- -- Apply top offset (distance from parent's content box top edge)
- if child.top then
- child.y = parent.y + parent.padding.top + child.top
- end
-
- -- Apply bottom offset (distance from parent's content box bottom edge)
- -- BORDER-BOX MODEL: Use border-box dimensions for positioning
- if child.bottom then
- local elementBorderBoxHeight = child:getBorderBoxHeight()
- child.y = parent.y + parent.padding.top + parent.height - child.bottom - elementBorderBoxHeight
- end
-
- -- Apply left offset (distance from parent's content box left edge)
- if child.left then
- child.x = parent.x + parent.padding.left + child.left
- end
-
- -- Apply right offset (distance from parent's content box right edge)
- -- BORDER-BOX MODEL: Use border-box dimensions for positioning
- if child.right then
- local elementBorderBoxWidth = child:getBorderBoxWidth()
- child.x = parent.x + parent.padding.left + parent.width - child.right - elementBorderBoxWidth
- end
- end
-end
-
---- Calculate flex item sizes based on flexGrow, flexShrink, flexBasis
---- Implements CSS flexbox sizing algorithm
----@param children table Array of child elements in the flex line
----@param availableMainSize number Available space in main axis
----@param gap number Gap between items
----@param isHorizontal boolean Whether main axis is horizontal
----@param defaultFlexShrink number? Default flex-shrink to use when child.flexShrink is nil
----@return table mainSizes Array of calculated main sizes for each child
-function LayoutEngine:_calculateFlexSizes(children, availableMainSize, gap, isHorizontal, defaultFlexShrink)
- local implicitFlexShrink = defaultFlexShrink
- if implicitFlexShrink == nil then
- implicitFlexShrink = 1
- end
-
- local function getResolvedFlexShrink(child)
- if child._hasExplicitFlexShrink then
- return child.flexShrink
- end
- return implicitFlexShrink
- end
-
- local childCount = #children
- local totalGaps = math.max(0, childCount - 1) * gap
- local availableForContent = availableMainSize - totalGaps
- local viewportWidth, viewportHeight = self._Units.getViewport()
-
- -- Step 1: Calculate hypothetical main sizes (flex basis resolution)
- local hypotheticalSizes = {}
- local flexBases = {}
- local totalFlexBasis = 0
-
- local function resolveDeclaredMainSize(child)
- local axisUnits = nil
- if child.units then
- axisUnits = isHorizontal and child.units.width or child.units.height
- end
-
- if not axisUnits or axisUnits.unit == "auto" or axisUnits.value == nil then
- return nil
- end
-
- local resolved =
- self._Units.resolve(axisUnits.value, axisUnits.unit, viewportWidth, viewportHeight, availableMainSize)
-
- if type(resolved) == "number" then
- return math.max(0, resolved)
- end
-
- return nil
- end
-
- for i, child in ipairs(children) do
- local flexBasis = child.flexBasis
- local hypotheticalSize
-
- -- Resolve flex-basis
- if flexBasis == "auto" then
- -- Use declared main size to avoid reusing a previously flexed runtime size
- hypotheticalSize = resolveDeclaredMainSize(child)
- if hypotheticalSize == nil then
- if isHorizontal then
- hypotheticalSize = child:getBorderBoxWidth()
- else
- hypotheticalSize = child:getBorderBoxHeight()
- end
- end
- elseif type(flexBasis) == "number" then
- hypotheticalSize = flexBasis
- elseif type(flexBasis) == "string" and child.units.flexBasis then
- -- Parse and resolve flex-basis with units
- local value, unit = child.units.flexBasis.value, child.units.flexBasis.unit
- hypotheticalSize = self._Units.resolve(value, unit, viewportWidth, viewportHeight, availableMainSize)
- else
- -- Fallback to element's natural size
- if isHorizontal then
- hypotheticalSize = child:getBorderBoxWidth()
- else
- hypotheticalSize = child:getBorderBoxHeight()
- end
- end
-
- -- Add margins to hypothetical size
- local childMargin = child.margin
- if isHorizontal then
- hypotheticalSize = hypotheticalSize + childMargin.left + childMargin.right
- else
- hypotheticalSize = hypotheticalSize + childMargin.top + childMargin.bottom
- end
-
- flexBases[i] = hypotheticalSize
- hypotheticalSizes[i] = hypotheticalSize
- totalFlexBasis = totalFlexBasis + hypotheticalSize
- end
-
- -- Step 2: Determine if we need to grow or shrink
- local freeSpace = availableForContent - totalFlexBasis
-
- -- Step 3a: Handle positive free space (GROW)
- if freeSpace > 0 then
- local totalFlexGrow = 0
- for _, child in ipairs(children) do
- totalFlexGrow = totalFlexGrow + (child.flexGrow or 0)
- end
-
- if totalFlexGrow > 0 then
- -- Distribute free space proportionally to flex-grow values
- for i, child in ipairs(children) do
- local flexGrow = child.flexGrow or 0
- if flexGrow > 0 then
- local growAmount = (flexGrow / totalFlexGrow) * freeSpace
- hypotheticalSizes[i] = hypotheticalSizes[i] + growAmount
- end
- end
- end
- -- Step 3b: Handle negative free space (SHRINK)
- elseif freeSpace < 0 then
- local totalFlexShrink = 0
- local totalScaledShrinkFactor = 0
-
- for i, child in ipairs(children) do
- local flexShrink = getResolvedFlexShrink(child)
- totalFlexShrink = totalFlexShrink + flexShrink
- -- Scaled shrink factor = flex-shrink × flex-basis
- totalScaledShrinkFactor = totalScaledShrinkFactor + (flexShrink * flexBases[i])
- end
-
- if totalScaledShrinkFactor > 0 then
- -- Distribute shrinkage proportionally to (flex-shrink × flex-basis)
- for i, child in ipairs(children) do
- local flexShrink = getResolvedFlexShrink(child)
- if flexShrink > 0 then
- local scaledShrinkFactor = flexShrink * flexBases[i]
- local shrinkAmount = (scaledShrinkFactor / totalScaledShrinkFactor) * math.abs(freeSpace)
- hypotheticalSizes[i] = math.max(0, hypotheticalSizes[i] - shrinkAmount)
- end
- end
- end
- end
-
- -- Step 4: Return final main sizes (excluding margins), clamped to per-child min/max
- local mainSizes = {}
- for i, child in ipairs(children) do
- local childMargin = child.margin
- local marginSum = isHorizontal and (childMargin.left + childMargin.right) or (childMargin.top + childMargin.bottom)
- local minBound = isHorizontal and child.minWidth or child.minHeight
- local maxBound = isHorizontal and child.maxWidth or child.maxHeight
- mainSizes[i] = LayoutEngine._Utils.clamp(math.max(0, hypotheticalSizes[i] - marginSum), minBound, maxBound)
- end
-
- return mainSizes
-end
-
---- Layout children within this element according to positioning mode
-function LayoutEngine:layoutChildren()
- -- Start performance timing first (before any early returns)
- local timerName = nil
- if LayoutEngine._Performance and LayoutEngine._Performance.enabled and self.element then
- -- Use memory address to make timer name unique per element instance
- timerName = "layout_" .. (self.element.id or tostring(self.element):match("0x%x+") or "unknown")
- LayoutEngine._Performance:startTimer(timerName)
- end
-
- if self.element == nil then
- return
- end
-
- -- Check if layout can be skipped (memoization optimization)
- if self:_canSkipLayout() then
- if timerName and LayoutEngine._Performance then
- LayoutEngine._Performance:stopTimer(timerName)
- end
- return
- end
-
- -- Track layout recalculations for performance warnings
- self:_trackLayoutRecalculation()
-
- -- Handle grid layout
- if self.positioning == self._Positioning.GRID then
- self._Grid.layoutGridItems(self.element)
-
- -- Stop performance timing
- if timerName and LayoutEngine._Performance then
- LayoutEngine._Performance:stopTimer(timerName)
- end
- return
- end
-
- local childCount = #self.element.children
-
- if childCount == 0 then
- -- Stop performance timing
- if timerName and LayoutEngine._Performance then
- LayoutEngine._Performance:stopTimer(timerName)
- end
- return
- end
-
- -- Get flex children (children that participate in flex layout)
- -- Exclude display=false (CSS display:none) and explicitly absolute children
- local flexChildren = {}
- for _, child in ipairs(self.element.children) do
- local isFlexChild = not (child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute)
- and child.display ~= false
- if isFlexChild then
- table.insert(flexChildren, child)
-
- -- Warn if child uses percentage sizing but parent has autosizing
- if child.units and child.units.width then
- if child.units.width.unit == "%" and self.element.autosizing and self.element.autosizing.width then
- self.element:_warnIfPercentageWithAutoSizing(child, "width")
- end
- end
- if child.units and child.units.height then
- if child.units.height.unit == "%" and self.element.autosizing and self.element.autosizing.height then
- self.element:_warnIfPercentageWithAutoSizing(child, "height")
- end
- end
- end
- end
-
- -- CSS-compliant behavior: absolutely positioned elements are completely removed from normal flow
- -- They do NOT reserve space or affect flex layout calculations at all
-
- -- If no flex children, skip flex layout but still position absolute children
- if #flexChildren == 0 then
- -- Position absolutely positioned children even when there are no flex children
- for i, child in ipairs(self.element.children) do
- if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute and child.display ~= false then
- self:applyPositioningOffsets(child)
-
- -- If child has children, layout them after position change
- if #child.children > 0 then
- child:layoutChildren()
- end
- end
- end
-
- -- Detect overflow after children positioning
- if self.element._detectOverflow then
- self.element:_detectOverflow()
- end
-
- -- Stop performance timing
- if timerName and LayoutEngine._Performance then
- LayoutEngine._Performance:stopTimer(timerName)
- end
- return
- end
-
- -- Calculate available space (accounting for padding only, NOT absolute children)
- -- BORDER-BOX MODEL: element.width and element.height are already content dimensions (padding subtracted)
- local availableMainSize = 0
- local availableCrossSize = 0
-
- -- Reserve space for scrollbars if needed (reserve-space mode)
- local scrollbarReservedWidth = 0
- local scrollbarReservedHeight = 0
- if self.element._scrollManager and self.element._scrollManager.scrollbarPlacement == "reserve-space" then
- scrollbarReservedWidth, scrollbarReservedHeight = self.element._scrollManager:getReservedSpace(self.element)
- end
-
- if self:_isHorizontal() then
- availableMainSize = self.element.width - scrollbarReservedWidth
- availableCrossSize = self.element.height - scrollbarReservedHeight
- else
- availableMainSize = self.element.height - scrollbarReservedHeight
- availableCrossSize = self.element.width - scrollbarReservedWidth
- end
-
- -- Keep percentage-sized children in sync when container dimensions change.
- -- Managed select frames rely on this so `width = "100%"` options expand with the dropdown.
- if scrollbarReservedWidth > 0 or scrollbarReservedHeight > 0 or self.element:_shouldSyncPercentageDimensions() then
- local isHorizontal = self:_isHorizontal()
- for _, child in ipairs(flexChildren) do
- if isHorizontal then
- -- Horizontal flex: main-axis is width, cross-axis is height
- -- Adjust main-axis width if percentage-based
- if child.units and child.units.width and child.units.width.unit == "%" then
- local newBorderBoxWidth = LayoutEngine._Utils.clamp(
- (child.units.width.value / 100) * availableMainSize,
- child.minWidth,
- child.maxWidth
- )
- child._borderBoxWidth = newBorderBoxWidth
- child.width = math.max(0, newBorderBoxWidth - child.padding.left - child.padding.right)
- end
- -- Adjust cross-axis height if percentage-based
- if child.units and child.units.height and child.units.height.unit == "%" then
- local newBorderBoxHeight = LayoutEngine._Utils.clamp(
- (child.units.height.value / 100) * availableCrossSize,
- child.minHeight,
- child.maxHeight
- )
- child._borderBoxHeight = newBorderBoxHeight
- child.height = math.max(0, newBorderBoxHeight - child.padding.top - child.padding.bottom)
- end
- else
- -- Vertical flex: main-axis is height, cross-axis is width
- -- Adjust main-axis height if percentage-based
- if child.units and child.units.height and child.units.height.unit == "%" then
- local newBorderBoxHeight = LayoutEngine._Utils.clamp(
- (child.units.height.value / 100) * availableMainSize,
- child.minHeight,
- child.maxHeight
- )
- child._borderBoxHeight = newBorderBoxHeight
- child.height = math.max(0, newBorderBoxHeight - child.padding.top - child.padding.bottom)
- end
- -- Adjust cross-axis width if percentage-based
- if child.units and child.units.width and child.units.width.unit == "%" then
- local rawBorderBoxWidth = (child.units.width.value / 100) * availableCrossSize
- local newBorderBoxWidth = LayoutEngine._Utils.clamp(
- self.element:_adjustCrossAxisPercentageWidth(child, rawBorderBoxWidth),
- child.minWidth,
- child.maxWidth
- )
- child._borderBoxWidth = newBorderBoxWidth
- child.width = math.max(0, newBorderBoxWidth - child.padding.left - child.padding.right)
- end
- end
- end
- end
-
- -- Handle flex wrap: create lines of children
- local lines = {}
-
- if self.flexWrap == self._FlexWrap.NOWRAP then
- -- All children go on one line
- lines[1] = flexChildren
- else
- -- Wrap children into multiple lines
- local currentLine = {}
- local currentLineSize = 0
-
- -- Performance optimization: hoist enum comparisons outside loop
- local isHorizontal = self:_isHorizontal()
- local gapSize = self.gap
- local viewportWidth, viewportHeight = self._Units.getViewport()
-
- local function resolveDeclaredMainSizeForWrap(child)
- local axisUnits = nil
- if child.units then
- axisUnits = isHorizontal and child.units.width or child.units.height
- end
-
- if not axisUnits or axisUnits.unit == "auto" or axisUnits.value == nil then
- return nil
- end
-
- local resolved =
- self._Units.resolve(axisUnits.value, axisUnits.unit, viewportWidth, viewportHeight, availableMainSize)
-
- if type(resolved) == "number" then
- return math.max(0, resolved)
- end
-
- return nil
- end
-
- for _, child in ipairs(flexChildren) do
- -- BORDER-BOX MODEL: Use border-box dimensions for layout calculations
- -- Include margins in size calculations
- -- Performance optimization: hoist margin table access
- local childMargin = child.margin
- local childMainSize = 0
- local childMainMargin = 0
- local declaredMainSize = resolveDeclaredMainSizeForWrap(child)
- if isHorizontal then
- childMainSize = declaredMainSize or child:getBorderBoxWidth()
- childMainMargin = childMargin.left + childMargin.right
- else
- childMainSize = declaredMainSize or child:getBorderBoxHeight()
- childMainMargin = childMargin.top + childMargin.bottom
- end
- local childTotalMainSize = childMainSize + childMainMargin
-
- -- Check if adding this child would exceed the available space
- local lineSpacing = #currentLine > 0 and gapSize or 0
- if #currentLine > 0 and currentLineSize + lineSpacing + childTotalMainSize > availableMainSize then
- -- Start a new line
- if #currentLine > 0 then
- table.insert(lines, currentLine)
- end
- currentLine = { child }
- currentLineSize = childTotalMainSize
- else
- -- Add to current line
- table.insert(currentLine, child)
- currentLineSize = currentLineSize + lineSpacing + childTotalMainSize
- end
- end
-
- -- Add the last line if it has children
- if #currentLine > 0 then
- table.insert(lines, currentLine)
- end
-
- -- Handle wrap-reverse: reverse the order of lines
- if self.flexWrap == self._FlexWrap.WRAP_REVERSE then
- local reversedLines = {}
- for i = #lines, 1, -1 do
- table.insert(reversedLines, lines[i])
- end
- lines = reversedLines
- end
- end
-
- -- Apply flex sizing to each line BEFORE calculating line heights
- -- Performance optimization: hoist enum comparison outside loop
- local isHorizontal = self:_isHorizontal()
- local mainAxisOverflow = nil
- if self:_isHorizontal() then
- mainAxisOverflow = self.element.overflowX or self.element.overflow
- else
- mainAxisOverflow = self.element.overflowY or self.element.overflow
- end
- local preserveMainAxisOverflow = (mainAxisOverflow == "scroll" or mainAxisOverflow == "auto")
- local defaultFlexShrink = preserveMainAxisOverflow and 0 or 1
-
- for lineIndex, line in ipairs(lines) do
- -- Check if any child in this line needs flex sizing.
- -- For scroll/auto in the main axis, keep implicit shrink at 0 so overflow can scroll.
- local needsFlexSizing = false
- for _, child in ipairs(line) do
- local flexGrow = child.flexGrow or 0
- local flexBasis = child.flexBasis
- local resolvedFlexShrink = defaultFlexShrink
- if child._hasExplicitFlexShrink then
- resolvedFlexShrink = child.flexShrink
- end
-
- if flexGrow > 0 or (flexBasis and flexBasis ~= "auto") or resolvedFlexShrink > 0 then
- needsFlexSizing = true
- break
- end
- end
-
- -- Only apply flex sizing if needed
- if needsFlexSizing then
- -- Calculate flex sizes for this line
- local mainSizes = self:_calculateFlexSizes(line, availableMainSize, self.gap, isHorizontal, defaultFlexShrink)
-
- -- Apply calculated sizes to children
- for i, child in ipairs(line) do
- local mainSize = mainSizes[i]
-
- if isHorizontal then
- -- Update width for horizontal flex
- child._borderBoxWidth = mainSize
- child.width = math.max(0, mainSize - child.padding.left - child.padding.right)
- -- Invalidate width cache
- child._borderBoxWidthCache = nil
- else
- -- Update height for vertical flex
- child._borderBoxHeight = mainSize
- child.height = math.max(0, mainSize - child.padding.top - child.padding.bottom)
- -- Invalidate height cache
- child._borderBoxHeightCache = nil
- end
-
- -- Trigger layout for child's children if any
- if #child.children > 0 then
- child:layoutChildren()
- end
- end
- end
- end
-
- -- Calculate line positions and heights (including child padding)
- -- Performance optimization: preallocate array if possible
- local lineHeights = table.create and table.create(#lines) or {}
- local totalLinesHeight = 0
-
- -- Performance optimization: hoist enum comparison outside loop (already hoisted above)
- -- local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
-
- for lineIndex, line in ipairs(lines) do
- local maxCrossSize = 0
- for _, child in ipairs(line) do
- -- BORDER-BOX MODEL: Use border-box dimensions for layout calculations
- -- Include margins in cross-axis size calculations
- -- Performance optimization: hoist margin table access
- local childMargin = child.margin
- local childCrossSize = 0
- local childCrossMargin = 0
- if isHorizontal then
- childCrossSize = child:getBorderBoxHeight()
- childCrossMargin = childMargin.top + childMargin.bottom
- else
- childCrossSize = child:getBorderBoxWidth()
- childCrossMargin = childMargin.left + childMargin.right
- end
- local childTotalCrossSize = childCrossSize + childCrossMargin
- maxCrossSize = math.max(maxCrossSize, childTotalCrossSize)
- end
- lineHeights[lineIndex] = maxCrossSize
- totalLinesHeight = totalLinesHeight + maxCrossSize
- end
-
- -- Account for gaps between lines
- local lineGaps = math.max(0, #lines - 1) * self.gap
- totalLinesHeight = totalLinesHeight + lineGaps
-
- -- For single line layouts, CENTER, FLEX_END and STRETCH should use full cross size
- if #lines == 1 then
- if
- self.alignItems == self._AlignItems.STRETCH
- or self.alignItems == self._AlignItems.CENTER
- or self.alignItems == self._AlignItems.FLEX_END
- then
- -- STRETCH, CENTER, and FLEX_END should use full available cross size
- lineHeights[1] = availableCrossSize
- totalLinesHeight = availableCrossSize
- end
- -- CENTER and FLEX_END should preserve natural child dimensions
- -- and only affect positioning within the available space
- end
-
- -- Calculate starting position for lines based on alignContent
- local lineStartPos = 0
- local lineSpacing = self.gap
- local freeLineSpace = availableCrossSize - totalLinesHeight
-
- -- Apply AlignContent logic for both single and multiple lines
- if self.alignContent == self._AlignContent.FLEX_START then
- lineStartPos = 0
- elseif self.alignContent == self._AlignContent.CENTER then
- lineStartPos = freeLineSpace / 2
- elseif self.alignContent == self._AlignContent.FLEX_END then
- lineStartPos = freeLineSpace
- elseif self.alignContent == self._AlignContent.SPACE_BETWEEN then
- lineStartPos = 0
- if #lines > 1 then
- lineSpacing = self.gap + (freeLineSpace / (#lines - 1))
- end
- elseif self.alignContent == self._AlignContent.SPACE_AROUND then
- local spaceAroundEach = freeLineSpace / #lines
- lineStartPos = spaceAroundEach / 2
- lineSpacing = self.gap + spaceAroundEach
- elseif self.alignContent == self._AlignContent.STRETCH then
- lineStartPos = 0
- if #lines > 1 and freeLineSpace > 0 then
- lineSpacing = self.gap + (freeLineSpace / #lines)
- -- Distribute extra space to line heights (only if positive)
- local extraPerLine = freeLineSpace / #lines
- for i = 1, #lineHeights do
- lineHeights[i] = lineHeights[i] + extraPerLine
- end
- end
- end
-
- -- Position children within each line
- local currentCrossPos = lineStartPos
-
- for lineIndex, line in ipairs(lines) do
- local lineHeight = lineHeights[lineIndex]
-
- -- Calculate total size of children in this line (including padding and margins)
- -- BORDER-BOX MODEL: Use border-box dimensions for layout calculations
- -- Performance optimization: hoist flexDirection check outside loop
- local isHorizontal = self:_isHorizontal()
- local totalChildrenSize = 0
- for _, child in ipairs(line) do
- local childMargin = child.margin
- if isHorizontal then
- totalChildrenSize = totalChildrenSize + child:getBorderBoxWidth() + childMargin.left + childMargin.right
- else
- totalChildrenSize = totalChildrenSize + child:getBorderBoxHeight() + childMargin.top + childMargin.bottom
- end
- end
-
- local totalGapSize = math.max(0, #line - 1) * self.gap
- local totalContentSize = totalChildrenSize + totalGapSize
- local freeSpace = availableMainSize - totalContentSize
-
- -- Calculate initial position and spacing based on justifyContent
- local startPos = 0
- local itemSpacing = self.gap
-
- if self.justifyContent == self._JustifyContent.FLEX_START then
- startPos = 0
- elseif self.justifyContent == self._JustifyContent.CENTER then
- startPos = math.max(0, freeSpace / 2)
- elseif self.justifyContent == self._JustifyContent.FLEX_END then
- startPos = math.max(0, freeSpace)
- elseif self.justifyContent == self._JustifyContent.SPACE_BETWEEN then
- startPos = 0
- if #line > 1 and freeSpace > 0 then
- itemSpacing = self.gap + (freeSpace / (#line - 1))
- end
- elseif self.justifyContent == self._JustifyContent.SPACE_AROUND then
- if freeSpace > 0 then
- local spaceAroundEach = freeSpace / #line
- startPos = spaceAroundEach / 2
- itemSpacing = self.gap + spaceAroundEach
- end
- elseif self.justifyContent == self._JustifyContent.SPACE_EVENLY then
- if freeSpace > 0 then
- local spaceBetween = freeSpace / (#line + 1)
- startPos = spaceBetween
- itemSpacing = self.gap + spaceBetween
- end
- end
-
- -- Position children in this line
- local currentMainPos = startPos
-
- -- Performance optimization: hoist frequently accessed element properties
- local elementX = self.element.x
- local elementY = self.element.y
- local elementPadding = self.element.padding
- local elementPaddingLeft = elementPadding.left
- local elementPaddingTop = elementPadding.top
- local alignItems = self.alignItems
- local alignSelf_AUTO = self._AlignSelf.AUTO
- local alignItems_FLEX_START = self._AlignItems.FLEX_START
- local alignItems_CENTER = self._AlignItems.CENTER
- local alignItems_FLEX_END = self._AlignItems.FLEX_END
- local alignItems_STRETCH = self._AlignItems.STRETCH
-
- for _, child in ipairs(line) do
- -- Performance optimization: hoist child table accesses
- local childMargin = child.margin
- local childPadding = child.padding
- local childAutosizing = child.autosizing
-
- -- Determine effective cross-axis alignment
- local effectiveAlign = child.alignSelf
- if effectiveAlign == nil or effectiveAlign == alignSelf_AUTO then
- effectiveAlign = alignItems
- end
-
- if self:_isHorizontal() then
- -- Horizontal layout: main axis is X, cross axis is Y
- -- Position child at border box (x, y represents top-left including padding)
- -- CSS-compliant: absolute children don't affect flex positioning, so no reserved space offset
- local childMarginLeft = childMargin.left
- child.x = elementX + elementPaddingLeft + currentMainPos + childMarginLeft
-
- -- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations
- local childBorderBoxHeight = child:getBorderBoxHeight()
- local childMarginTop = childMargin.top
- local childMarginBottom = childMargin.bottom
- local childTotalCrossSize = childBorderBoxHeight + childMarginTop + childMarginBottom
-
- if effectiveAlign == alignItems_FLEX_START then
- child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop
- elseif effectiveAlign == alignItems_CENTER then
- child.y = elementY
- + elementPaddingTop
- + currentCrossPos
- + ((lineHeight - childTotalCrossSize) / 2)
- + childMarginTop
- elseif effectiveAlign == alignItems_FLEX_END then
- child.y = elementY + elementPaddingTop + currentCrossPos + lineHeight - childTotalCrossSize + childMarginTop
- elseif effectiveAlign == alignItems_STRETCH then
- -- STRETCH: Only apply if height was not explicitly set
- if childAutosizing and childAutosizing.height then
- -- STRETCH: Set border-box height to lineHeight minus margins, content area shrinks to fit
- local availableHeight = LayoutEngine._Utils.clamp(
- lineHeight - childMarginTop - childMarginBottom,
- child.minHeight,
- child.maxHeight
- )
- child._borderBoxHeight = availableHeight
- child.height = math.max(0, availableHeight - childPadding.top - childPadding.bottom)
- end
- child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop
- end
-
- -- Apply positioning offsets (top, right, bottom, left)
- self:applyPositioningOffsets(child)
-
- -- If child has children, re-layout them after position change
- if #child.children > 0 then
- child:layoutChildren()
- end
-
- -- Advance position by child's border-box width plus margins
- currentMainPos = currentMainPos + child:getBorderBoxWidth() + childMarginLeft + childMargin.right + itemSpacing
- else
- -- Vertical layout: main axis is Y, cross axis is X
- -- Position child at border box (x, y represents top-left including padding)
- -- CSS-compliant: absolute children don't affect flex positioning, so no reserved space offset
- local childMarginTop = childMargin.top
- child.y = elementY + elementPaddingTop + currentMainPos + childMarginTop
-
- -- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations
- local childBorderBoxWidth = child:getBorderBoxWidth()
- local childMarginLeft = childMargin.left
- local childMarginRight = childMargin.right
- local childTotalCrossSize = childBorderBoxWidth + childMarginLeft + childMarginRight
- local elementPaddingLeft = elementPadding.left
-
- if effectiveAlign == alignItems_FLEX_START then
- child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft
- elseif effectiveAlign == alignItems_CENTER then
- child.x = elementX
- + elementPaddingLeft
- + currentCrossPos
- + ((lineHeight - childTotalCrossSize) / 2)
- + childMarginLeft
- elseif effectiveAlign == alignItems_FLEX_END then
- child.x = elementX + elementPaddingLeft + currentCrossPos + lineHeight - childTotalCrossSize + childMarginLeft
- elseif effectiveAlign == alignItems_STRETCH then
- -- STRETCH: Only apply if width was not explicitly set
- if childAutosizing and childAutosizing.width then
- -- STRETCH: Set border-box width to lineHeight minus margins, content area shrinks to fit
- local availableWidth =
- LayoutEngine._Utils.clamp(lineHeight - childMarginLeft - childMarginRight, child.minWidth, child.maxWidth)
- child._borderBoxWidth = availableWidth
- child.width = math.max(0, availableWidth - childPadding.left - childPadding.right)
- end
- child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft
- end
-
- -- Apply positioning offsets (top, right, bottom, left)
- self:applyPositioningOffsets(child)
-
- -- If child has children, re-layout them after position change
- if #child.children > 0 then
- child:layoutChildren()
- end
-
- -- Advance position by child's border-box height plus margins
- currentMainPos = currentMainPos
- + child:getBorderBoxHeight()
- + child.margin.top
- + child.margin.bottom
- + itemSpacing
- end
- end
-
- -- Move to next line position
- currentCrossPos = currentCrossPos + lineHeight + lineSpacing
- end
-
- -- Position explicitly absolute children after flex layout
- for i, child in ipairs(self.element.children) do
- if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute and child.display ~= false then
- -- Apply positioning offsets (top, right, bottom, left)
- self:applyPositioningOffsets(child)
-
- -- If child has children, layout them after position change
- if #child.children > 0 then
- child:layoutChildren()
- end
- end
- end
-
- -- flex-direction: row-reverse / column-reverse — mirror the main-axis
- -- position of each flex child relative to the container content area, and
- -- shift the child's subtree by the same delta so descendants follow.
- -- Cross-axis positions and absolute children are not affected.
- if self:_isReverse() then
- local parent = self.element
- local padLeft = parent.padding.left
- local padTop = parent.padding.top
- local contentW = parent.width
- local contentH = parent.height
- local mirrorHorizontal = self:_isHorizontal()
-
- for _, child in ipairs(flexChildren) do
- if mirrorHorizontal then
- local distFromLeft = child.x - parent.x - padLeft
- local childW = child:getBorderBoxWidth()
- local newDistFromLeft = contentW - distFromLeft - childW
- local dx = newDistFromLeft - distFromLeft
- if dx ~= 0 then
- shiftSubtree(child, dx, 0)
- end
- else
- local distFromTop = child.y - parent.y - padTop
- local childH = child:getBorderBoxHeight()
- local newDistFromTop = contentH - distFromTop - childH
- local dy = newDistFromTop - distFromTop
- if dy ~= 0 then
- shiftSubtree(child, 0, dy)
- end
- end
- end
- end
-
- -- position: relative — shift each in-flow child by (left or -right,
- -- top or -bottom) after the flex flow (and row-reverse mirroring) has
- -- placed it, so the offset is a pure visual delta that doesn't influence
- -- siblings' flow positions. Per CSS, `top` wins over `bottom` and `left`
- -- over `right` when both are set. Static/absolute children are unaffected
- -- (absolute uses applyPositioningOffsets; flex-participating children
- -- dropped the offsets and emitted LAY_011 at construction). Runs for every
- -- container type so relative children in relative containers also honor offsets.
- for _, child in ipairs(self.element.children) do
- if child.positioning == self._Positioning.RELATIVE and child.display ~= false then
- local dx, dy = 0, 0
- if child.top then
- dy = child.top
- elseif child.bottom then
- dy = -child.bottom
- end
- if child.left then
- dx = child.left
- elseif child.right then
- dx = -child.right
- end
- if dx ~= 0 or dy ~= 0 then
- shiftSubtree(child, dx, dy)
- end
- end
- end
-
- -- Detect overflow after children are laid out
- if self.element._detectOverflow then
- self.element:_detectOverflow()
- end
-
- -- Stop performance timing
- if timerName and LayoutEngine._Performance then
- LayoutEngine._Performance:stopTimer(timerName)
- end
-end
-
---- Simulate wrapping children into lines for auto-sizing calculations
----@param children table Array of child elements
----@param availableSize number Available space in main axis
----@param isHorizontal boolean True if flex direction is horizontal
----@return table Array of lines, where each line is an array of children
-function LayoutEngine:_simulateWrap(children, availableSize, isHorizontal)
- local lines = {}
- local currentLine = {}
- local currentLineSize = 0
-
- for _, child in ipairs(children) do
- -- Calculate child size in main axis (including margins)
- local childMainSize = 0
- local childMainMargin = 0
- if isHorizontal then
- childMainSize = child:getBorderBoxWidth()
- if child.margin then
- childMainMargin = child.margin.left + child.margin.right
- end
- else
- childMainSize = child:getBorderBoxHeight()
- if child.margin then
- childMainMargin = child.margin.top + child.margin.bottom
- end
- end
- local childTotalMainSize = childMainSize + childMainMargin
-
- -- Check if adding this child would exceed the available space
- local lineSpacing = #currentLine > 0 and self.gap or 0
- if #currentLine > 0 and currentLineSize + lineSpacing + childTotalMainSize > availableSize then
- -- Start a new line
- table.insert(lines, currentLine)
- currentLine = { child }
- currentLineSize = childTotalMainSize
- else
- -- Add to current line
- table.insert(currentLine, child)
- currentLineSize = currentLineSize + lineSpacing + childTotalMainSize
- end
- end
-
- -- Add the last line if it has children
- if #currentLine > 0 then
- table.insert(lines, currentLine)
- end
-
- return lines
-end
-
---- Calculate auto width based on children
----@return number
-function LayoutEngine:calculateAutoWidth()
- if self.element == nil then
- return 0
- end
-
- -- BORDER-BOX MODEL: Calculate content width, caller will add padding to get border-box
- local contentWidth = self.element:calculateTextWidth()
- if not self.element.children or #self.element.children == 0 then
- return contentWidth
- end
-
- -- Get flex children (children that participate in flex layout)
- -- Exclude display=false (CSS display:none) and explicitly absolute children
- local flexChildren = {}
- for _, child in ipairs(self.element.children) do
- if not child._explicitlyAbsolute and child.display ~= false then
- table.insert(flexChildren, child)
- end
- end
-
- if #flexChildren == 0 then
- return contentWidth
- end
-
- local isHorizontal = self:_isHorizontal()
-
- if isHorizontal then
- -- HORIZONTAL flex with potential wrapping
- if self.flexWrap ~= self._FlexWrap.NOWRAP and self.element.width and self.element.width > 0 then
- -- Container has explicit width and wrapping enabled - calculate based on wrapped lines
- local availableWidth = self.element.width
- local lines = self:_simulateWrap(flexChildren, availableWidth, true)
-
- -- Find the widest line
- local maxLineWidth = contentWidth
- for _, line in ipairs(lines) do
- local lineWidth = 0
- for i, child in ipairs(line) do
- local childBorderBoxWidth = child:getBorderBoxWidth()
- local childMarginH = 0
- if child.margin then
- childMarginH = child.margin.left + child.margin.right
- end
- lineWidth = lineWidth + childBorderBoxWidth + childMarginH
- if i < #line then
- lineWidth = lineWidth + self.gap
- end
- end
- maxLineWidth = math.max(maxLineWidth, lineWidth)
- end
- return maxLineWidth
- else
- -- No wrapping or no explicit width - sum all children on one line
- local totalWidth = contentWidth
- for i, child in ipairs(flexChildren) do
- local childBorderBoxWidth = child:getBorderBoxWidth()
- local childMarginH = 0
- if child.margin then
- childMarginH = child.margin.left + child.margin.right
- end
- totalWidth = totalWidth + childBorderBoxWidth + childMarginH
- if i < #flexChildren then
- totalWidth = totalWidth + self.gap
- end
- end
- return totalWidth
- end
- else
- -- VERTICAL flex - return max child width (including margins)
- local maxWidth = contentWidth
- for _, child in ipairs(flexChildren) do
- local childBorderBoxWidth = child:getBorderBoxWidth()
- childBorderBoxWidth = self.element:_adjustAutoWidthChildBorderBoxForManagedSelect(child, childBorderBoxWidth)
- local childMarginH = 0
- if child.margin then
- childMarginH = child.margin.left + child.margin.right
- end
- maxWidth = math.max(maxWidth, childBorderBoxWidth + childMarginH)
- end
- return maxWidth
- end
-end
-
----@return number
-function LayoutEngine:calculateAutoHeight()
- if self.element == nil then
- return 0
- end
-
- local height = self.element:calculateTextHeight()
- if not self.element.children or #self.element.children == 0 then
- return height
- end
-
- -- Get flex children (children that participate in flex layout)
- -- Exclude display=false (CSS display:none) and explicitly absolute children
- local flexChildren = {}
- for _, child in ipairs(self.element.children) do
- if not child._explicitlyAbsolute and child.display ~= false then
- table.insert(flexChildren, child)
- end
- end
-
- if #flexChildren == 0 then
- return height
- end
-
- local isVertical = not self:_isHorizontal()
-
- if isVertical then
- -- VERTICAL flex with potential wrapping
- if self.flexWrap ~= self._FlexWrap.NOWRAP and self.element.height and self.element.height > 0 then
- -- Container has explicit height and wrapping enabled - calculate based on wrapped lines
- local availableHeight = self.element.height
- local lines = self:_simulateWrap(flexChildren, availableHeight, false)
-
- -- Sum all line heights
- local totalLinesHeight = height
- for i, line in ipairs(lines) do
- local lineHeight = 0
- for _, child in ipairs(line) do
- local childBorderBoxHeight = child:getBorderBoxHeight()
- local childMarginV = 0
- if child.margin then
- childMarginV = child.margin.top + child.margin.bottom
- end
- lineHeight = math.max(lineHeight, childBorderBoxHeight + childMarginV)
- end
- totalLinesHeight = totalLinesHeight + lineHeight
- if i < #lines then
- totalLinesHeight = totalLinesHeight + self.gap
- end
- end
- return totalLinesHeight
- else
- -- No wrapping or no explicit height - sum all children on one line
- local totalHeight = height
- for i, child in ipairs(flexChildren) do
- local childBorderBoxHeight = child:getBorderBoxHeight()
- local childMarginV = 0
- if child.margin then
- childMarginV = child.margin.top + child.margin.bottom
- end
- totalHeight = totalHeight + childBorderBoxHeight + childMarginV
- if i < #flexChildren then
- totalHeight = totalHeight + self.gap
- end
- end
- return totalHeight
- end
- else
- -- HORIZONTAL flex with potential wrapping
- if self.flexWrap ~= self._FlexWrap.NOWRAP and self.element.width and self.element.width > 0 then
- -- Container has explicit width and wrapping enabled - calculate based on wrapped lines
- local availableWidth = self.element.width
- local lines = self:_simulateWrap(flexChildren, availableWidth, true)
-
- -- Sum all line heights (cross-axis for horizontal flex)
- local totalLinesHeight = height
- for i, line in ipairs(lines) do
- local lineHeight = 0
- for _, child in ipairs(line) do
- local childBorderBoxHeight = child:getBorderBoxHeight()
- local childMarginV = 0
- if child.margin then
- childMarginV = child.margin.top + child.margin.bottom
- end
- lineHeight = math.max(lineHeight, childBorderBoxHeight + childMarginV)
- end
- totalLinesHeight = totalLinesHeight + lineHeight
- if i < #lines then
- totalLinesHeight = totalLinesHeight + self.gap
- end
- end
- return totalLinesHeight
- else
- -- No wrapping or no explicit width - return max child height (including margins)
- local maxHeight = height
- for _, child in ipairs(flexChildren) do
- local childBorderBoxHeight = child:getBorderBoxHeight()
- local childMarginV = 0
- if child.margin then
- childMarginV = child.margin.top + child.margin.bottom
- end
- maxHeight = math.max(maxHeight, childBorderBoxHeight + childMarginV)
- end
- return maxHeight
- end
- end
-end
-
---- Recalculate units based on new viewport dimensions (for vw, vh, % units)
----@param newViewportWidth number
----@param newViewportHeight number
-function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
- if self.element == nil then
- return
- end
- local Units = self._Units
-
- -- Get updated scale factors
- local scaleX, scaleY = self._Context.getScaleFactors()
-
- -- Recalculate border-box width if using viewport or percentage units (skip auto-sized)
- -- Store in _borderBoxWidth temporarily, will calculate content width after padding is resolved
- if self.element.units.width.unit ~= "px" and self.element.units.width.unit ~= "auto" then
- local parentWidth = self.element.parent and self.element.parent.width or newViewportWidth
- self.element._borderBoxWidth = Units.resolve(
- self.element.units.width.value,
- self.element.units.width.unit,
- newViewportWidth,
- newViewportHeight,
- parentWidth
- )
- elseif self.element.units.width.unit == "px" and self.element.units.width.value and self._Context.baseScale then
- -- Reapply base scaling to pixel widths (border-box)
- self.element._borderBoxWidth = self.element.units.width.value * scaleX
- end
-
- -- Recalculate border-box height if using viewport or percentage units (skip auto-sized)
- -- Store in _borderBoxHeight temporarily, will calculate content height after padding is resolved
- if self.element.units.height.unit ~= "px" and self.element.units.height.unit ~= "auto" then
- local parentHeight = self.element.parent and self.element.parent.height or newViewportHeight
- self.element._borderBoxHeight = Units.resolve(
- self.element.units.height.value,
- self.element.units.height.unit,
- newViewportWidth,
- newViewportHeight,
- parentHeight
- )
- elseif self.element.units.height.unit == "px" and self.element.units.height.value and self._Context.baseScale then
- -- Reapply base scaling to pixel heights (border-box)
- self.element._borderBoxHeight = self.element.units.height.value * scaleY
- end
-
- -- Recalculate position if using viewport or percentage units
- -- Skip position recalculation for flex children (non-explicitly-absolute children with a parent)
- -- Their x/y is entirely controlled by the parent's layoutChildren() call
- local isFlexChild = self.element.parent and not self.element._explicitlyAbsolute
- if not isFlexChild then
- if self.element.units.x.unit ~= "px" then
- local parentWidth = self.element.parent and self.element.parent.width or newViewportWidth
- local baseX = self.element.parent and self.element.parent.x or 0
- local offsetX = Units.resolve(
- self.element.units.x.value,
- self.element.units.x.unit,
- newViewportWidth,
- newViewportHeight,
- parentWidth
- )
- self.element.x = baseX + offsetX
- else
- -- For pixel units, update position relative to parent's new position (with base scaling)
- if self.element.parent then
- local baseX = self.element.parent.x
- local scaledOffset = self._Context.baseScale and (self.element.units.x.value * scaleX)
- or self.element.units.x.value
- self.element.x = baseX + scaledOffset
- elseif self._Context.baseScale then
- -- Top-level element with pixel position - apply base scaling
- self.element.x = self.element.units.x.value * scaleX
- end
- end
-
- if self.element.units.y.unit ~= "px" then
- local parentHeight = self.element.parent and self.element.parent.height or newViewportHeight
- local baseY = self.element.parent and self.element.parent.y or 0
- local offsetY = Units.resolve(
- self.element.units.y.value,
- self.element.units.y.unit,
- newViewportWidth,
- newViewportHeight,
- parentHeight
- )
- self.element.y = baseY + offsetY
- else
- -- For pixel units, update position relative to parent's new position (with base scaling)
- if self.element.parent then
- local baseY = self.element.parent.y
- local scaledOffset = self._Context.baseScale and (self.element.units.y.value * scaleY)
- or self.element.units.y.value
- self.element.y = baseY + scaledOffset
- elseif self._Context.baseScale then
- -- Top-level element with pixel position - apply base scaling
- self.element.y = self.element.units.y.value * scaleY
- end
- end
- end
-
- -- Recalculate textSize if auto-scaling is enabled or using viewport/element-relative units
- if self.element.autoScaleText and self.element.units.textSize.value then
- local unit = self.element.units.textSize.unit
- local value = self.element.units.textSize.value
-
- if unit == "px" and self._Context.baseScale then
- -- With base scaling: scale pixel values relative to base resolution
- self.element.textSize = value * scaleY
- elseif unit == "px" then
- -- Without base scaling but auto-scaling enabled: text doesn't scale
- self.element.textSize = value
- elseif unit == "%" or unit == "vh" then
- -- Percentage and vh are relative to viewport height
- self.element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportHeight)
- elseif unit == "vw" then
- -- vw is relative to viewport width
- self.element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportWidth)
- else
- self.element.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, nil)
- end
-
- -- Apply min/max constraints (with base scaling)
- local minSize = self.element.minTextSize
- and (self._Context.baseScale and (self.element.minTextSize * scaleY) or self.element.minTextSize)
- local maxSize = self.element.maxTextSize
- and (self._Context.baseScale and (self.element.maxTextSize * scaleY) or self.element.maxTextSize)
-
- if minSize and self.element.textSize < minSize then
- self.element.textSize = minSize
- end
- if maxSize and self.element.textSize > maxSize then
- self.element.textSize = maxSize
- end
-
- -- Protect against too-small text sizes (minimum 1px)
- if self.element.textSize < 1 then
- self.element.textSize = 1 -- Minimum 1px
- end
- elseif self.element.units.textSize.unit == "px" and self.element.units.textSize.value and self._Context.baseScale then
- -- No auto-scaling but base scaling is set: reapply base scaling to pixel text sizes
- self.element.textSize = self.element.units.textSize.value * scaleY
-
- -- Protect against too-small text sizes (minimum 1px)
- if self.element.textSize < 1 then
- self.element.textSize = 1 -- Minimum 1px
- end
- end
-
- -- Final protection: ensure textSize is always at least 1px (catches all edge cases)
- if self.element.text and self.element.textSize and self.element.textSize < 1 then
- self.element.textSize = 1 -- Minimum 1px
- end
-
- -- Recalculate gap if using viewport or percentage units
- if self.element.units.gap.unit ~= "px" then
- local containerSize = (self:_isHorizontal())
- and (self.element.parent and self.element.parent.width or newViewportWidth)
- or (self.element.parent and self.element.parent.height or newViewportHeight)
- self.element.gap = Units.resolve(
- self.element.units.gap.value,
- self.element.units.gap.unit,
- newViewportWidth,
- newViewportHeight,
- containerSize
- )
- end
-
- -- Recalculate flexBasis if using viewport or percentage units
- if
- self.element.units.flexBasis
- and self.element.units.flexBasis.unit ~= "auto"
- and self.element.units.flexBasis.unit ~= "px"
- then
- local value, unit = self.element.units.flexBasis.value, self.element.units.flexBasis.unit
- -- flexBasis uses parent main-axis size for percentage resolution.
- local parentMainIsHorizontal = true
- if self.element.parent and self.element.parent.flexDirection then
- local pd = self.element.parent.flexDirection
- parentMainIsHorizontal = pd == self._FlexDirection.HORIZONTAL or pd == self._FlexDirection.HORIZONTAL_REVERSE
- end
- local parentSize = newViewportWidth
- if self.element.parent then
- if parentMainIsHorizontal then
- parentSize = self.element.parent.width
- else
- parentSize = self.element.parent.height
- end
- end
- local resolvedBasis = Units.resolve(value, unit, newViewportWidth, newViewportHeight, parentSize)
- if type(resolvedBasis) == "number" then
- self.element.flexBasis = resolvedBasis
- end
- end
-
- -- Recalculate spacing (padding/margin) if using viewport or percentage units
- -- For percentage-based padding:
- -- - If element has a parent: use parent's border-box dimensions (CSS spec for child elements)
- -- - If element has no parent: use element's own border-box dimensions (CSS spec for root elements)
- local parentBorderBoxWidth = self.element.parent and self.element.parent._borderBoxWidth
- or self.element._borderBoxWidth
- or newViewportWidth
- local parentBorderBoxHeight = self.element.parent and self.element.parent._borderBoxHeight
- or self.element._borderBoxHeight
- or newViewportHeight
-
- -- Handle shorthand properties first (horizontal/vertical)
- local resolvedHorizontalPadding = nil
- local resolvedVerticalPadding = nil
-
- if self.element.units.padding.horizontal and self.element.units.padding.horizontal.unit ~= "px" then
- resolvedHorizontalPadding = Units.resolve(
- self.element.units.padding.horizontal.value,
- self.element.units.padding.horizontal.unit,
- newViewportWidth,
- newViewportHeight,
- parentBorderBoxWidth
- )
- elseif self.element.units.padding.horizontal and self.element.units.padding.horizontal.value then
- resolvedHorizontalPadding = self.element.units.padding.horizontal.value
- end
-
- if self.element.units.padding.vertical and self.element.units.padding.vertical.unit ~= "px" then
- resolvedVerticalPadding = Units.resolve(
- self.element.units.padding.vertical.value,
- self.element.units.padding.vertical.unit,
- newViewportWidth,
- newViewportHeight,
- parentBorderBoxHeight
- )
- elseif self.element.units.padding.vertical and self.element.units.padding.vertical.value then
- resolvedVerticalPadding = self.element.units.padding.vertical.value
- end
- -- Resolve individual padding sides (with fallback to shorthand)
- for _, side in ipairs({ "top", "right", "bottom", "left" }) do
- -- Check if this side was explicitly set or if we should use shorthand
- local useShorthand = false
- if not self.element.units.padding[side].explicit then
- -- Not explicitly set, check if we have shorthand
- if side == "left" or side == "right" then
- useShorthand = resolvedHorizontalPadding ~= nil
- elseif side == "top" or side == "bottom" then
- useShorthand = resolvedVerticalPadding ~= nil
- end
- end
-
- if useShorthand then
- -- Use shorthand value
- if side == "left" or side == "right" then
- self.element.padding[side] = resolvedHorizontalPadding
- else
- self.element.padding[side] = resolvedVerticalPadding
- end
- elseif self.element.units.padding[side].unit ~= "px" then
- -- Recalculate non-pixel units
- local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth
- self.element.padding[side] = Units.resolve(
- self.element.units.padding[side].value,
- self.element.units.padding[side].unit,
- newViewportWidth,
- newViewportHeight,
- parentSize
- )
- end
- -- If unit is "px" and not using shorthand, value stays the same
- end
-
- -- Handle margin shorthand properties
- local resolvedHorizontalMargin = nil
- local resolvedVerticalMargin = nil
-
- if self.element.units.margin.horizontal and self.element.units.margin.horizontal.unit ~= "px" then
- resolvedHorizontalMargin = Units.resolve(
- self.element.units.margin.horizontal.value,
- self.element.units.margin.horizontal.unit,
- newViewportWidth,
- newViewportHeight,
- parentBorderBoxWidth
- )
- elseif self.element.units.margin.horizontal and self.element.units.margin.horizontal.value then
- resolvedHorizontalMargin = self.element.units.margin.horizontal.value
- end
-
- if self.element.units.margin.vertical and self.element.units.margin.vertical.unit ~= "px" then
- resolvedVerticalMargin = Units.resolve(
- self.element.units.margin.vertical.value,
- self.element.units.margin.vertical.unit,
- newViewportWidth,
- newViewportHeight,
- parentBorderBoxHeight
- )
- elseif self.element.units.margin.vertical and self.element.units.margin.vertical.value then
- resolvedVerticalMargin = self.element.units.margin.vertical.value
- end
-
- -- Resolve individual margin sides (with fallback to shorthand)
- for _, side in ipairs({ "top", "right", "bottom", "left" }) do
- -- Check if this side was explicitly set or if we should use shorthand
- local useShorthand = false
- if not self.element.units.margin[side].explicit then
- -- Not explicitly set, check if we have shorthand
- if side == "left" or side == "right" then
- useShorthand = resolvedHorizontalMargin ~= nil
- elseif side == "top" or side == "bottom" then
- useShorthand = resolvedVerticalMargin ~= nil
- end
- end
-
- if useShorthand then
- -- Use shorthand value
- if side == "left" or side == "right" then
- self.element.margin[side] = resolvedHorizontalMargin
- else
- self.element.margin[side] = resolvedVerticalMargin
- end
- elseif self.element.units.margin[side].unit ~= "px" then
- -- Recalculate non-pixel units
- local parentSize = (side == "top" or side == "bottom") and parentBorderBoxHeight or parentBorderBoxWidth
- self.element.margin[side] = Units.resolve(
- self.element.units.margin[side].value,
- self.element.units.margin[side].unit,
- newViewportWidth,
- newViewportHeight,
- parentSize
- )
- end
- -- If unit is "px" and not using shorthand, value stays the same
- end
-
- -- BORDER-BOX MODEL: Calculate content dimensions from border-box dimensions
- -- For explicitly-sized elements (non-auto), _borderBoxWidth/_borderBoxHeight were set earlier
- -- Now we calculate content width/height by subtracting padding
- -- Only recalculate if using viewport/percentage units (where _borderBoxWidth actually changed)
- if self.element.units.width.unit ~= "auto" and self.element.units.width.unit ~= "px" then
- -- _borderBoxWidth was recalculated for viewport/percentage units
- -- Calculate content width by subtracting padding
- self.element.width =
- math.max(0, self.element._borderBoxWidth - self.element.padding.left - self.element.padding.right)
- elseif self.element.units.width.unit == "auto" then
- -- For auto-sized elements, width is content width (calculated in resize method)
- -- Update border-box to include padding
- self.element._borderBoxWidth = self.element.width + self.element.padding.left + self.element.padding.right
- end
- -- For pixel units, width stays as-is (may have been manually modified)
-
- if self.element.units.height.unit ~= "auto" and self.element.units.height.unit ~= "px" then
- -- _borderBoxHeight was recalculated for viewport/percentage units
- -- Calculate content height by subtracting padding
- self.element.height =
- math.max(0, self.element._borderBoxHeight - self.element.padding.top - self.element.padding.bottom)
- elseif self.element.units.height.unit == "auto" then
- -- For auto-sized elements, height is content height (calculated in resize method)
- -- Update border-box to include padding
- self.element._borderBoxHeight = self.element.height + self.element.padding.top + self.element.padding.bottom
- end
- -- For pixel units, height stays as-is (may have been manually modified)
-
- -- Detect overflow after layout calculations
- if self.element._detectOverflow then
- self.element:_detectOverflow()
- end
-end
-
---- Check if layout can be skipped based on cached state (memoization)
----@return boolean canSkip True if layout hasn't changed and can be skipped
-function LayoutEngine:_canSkipLayout()
- if not self.element then
- return false
- end
-
- -- Performance optimization: Check dirty flags first (fastest check)
- -- If element or children are marked dirty, we must recalculate
- if self.element._dirty or self.element._childrenDirty then
- -- Clear dirty flags after acknowledging them
- self.element._dirty = false
- self.element._childrenDirty = false
- return false
- end
-
- -- If not dirty, check if layout inputs have actually changed (secondary check)
- local childrenCount = #self.element.children
- local containerWidth = self.element.width
- local containerHeight = self.element.height
- local containerX = self.element.x
- local containerY = self.element.y
-
- -- Generate simple hash of children dimensions + display state
- local childrenHash = ""
- for i, child in ipairs(self.element.children) do
- if i <= 5 then -- Only hash first 5 children for performance
- childrenHash = childrenHash .. child.width .. "x" .. child.height .. "d" .. tostring(child.display) .. ","
- end
- end
-
- local cache = self._layoutCache
-
- -- Check if layout inputs have changed
- if
- cache.childrenCount == childrenCount
- and cache.containerWidth == containerWidth
- and cache.containerHeight == containerHeight
- and cache.containerX == containerX
- and cache.containerY == containerY
- and cache.childrenHash == childrenHash
- then
- return true -- Layout hasn't changed, can skip
- end
-
- -- Update cache with current values
- cache.childrenCount = childrenCount
- cache.containerWidth = containerWidth
- cache.containerHeight = containerHeight
- cache.containerX = containerX
- cache.containerY = containerY
- cache.childrenHash = childrenHash
-
- return false -- Layout has changed, must recalculate
-end
-
---- Track layout recalculations and warn about excessive layouts
-function LayoutEngine:_trackLayoutRecalculation()
- if not LayoutEngine._Performance or not LayoutEngine._Performance.warningsEnabled then
- return
- end
-
- -- Get current frame count from Context
- local currentFrame = self._Context and self._Context._frameNumber or 0
-
- -- Reset counter on new frame
- if currentFrame ~= self._lastFrameCount then
- self._lastFrameCount = currentFrame
- self._layoutCount = 0
- end
-
- -- Increment layout count
- self._layoutCount = self._layoutCount + 1
-
- -- Warn if layout is recalculated excessively this frame
- if self._layoutCount >= 10 then
- local elementId = self.element and self.element.id or "unnamed"
- LayoutEngine._Performance:logWarning(
- string.format("excessive_layout_%s", elementId),
- "LayoutEngine",
- string.format("Layout recalculated %d times this frame for element '%s'", self._layoutCount, elementId),
- { layoutCount = self._layoutCount, elementId = elementId },
- "This may indicate a layout thrashing issue. Check for circular dependencies or dynamic sizing that triggers re-layout"
- )
- end
-end
-
-return LayoutEngine
diff --git a/libs/flexlove/modules/MemoryScanner.lua b/libs/flexlove/modules/MemoryScanner.lua
deleted file mode 100644
index 07f5ca70..00000000
--- a/libs/flexlove/modules/MemoryScanner.lua
+++ /dev/null
@@ -1,697 +0,0 @@
----@class MemoryScanner
----@field _StateManager table
----@field _Context table
----@field _ImageCache table
----@field _ErrorHandler table
-local MemoryScanner = {}
-
----Initialize MemoryScanner with dependencies
----@param deps {StateManager: table, Context: table, ImageCache: table, ErrorHandler: table}
-function MemoryScanner.init(deps)
- MemoryScanner._StateManager = deps.StateManager
- MemoryScanner._Context = deps.Context
- MemoryScanner._ImageCache = deps.ImageCache
- MemoryScanner._ErrorHandler = deps.ErrorHandler
-end
-
----Count items in a table
----@param tbl table
----@return number
-local function countTable(tbl)
- local count = 0
- for _ in pairs(tbl) do
- count = count + 1
- end
- return count
-end
-
----Calculate memory size estimate for a table (recursive)
----@param tbl table
----@param visited table? Tracking table to prevent circular references
----@param depth number? Current recursion depth
----@return number bytes Estimated memory usage in bytes
-local function estimateTableSize(tbl, visited, depth)
- if type(tbl) ~= "table" then
- return 0
- end
-
- visited = visited or {}
- depth = depth or 0
-
- -- Limit recursion depth to prevent stack overflow
- if depth > 10 then
- return 0
- end
-
- -- Check for circular references
- if visited[tbl] then
- return 0
- end
- visited[tbl] = true
-
- local size = 40 -- Base table overhead (approximate)
-
- for k, v in pairs(tbl) do
- -- Key size
- if type(k) == "string" then
- size = size + #k + 24 -- String overhead
- elseif type(k) == "number" then
- size = size + 8
- else
- size = size + 8 -- Reference
- end
-
- -- Value size
- if type(v) == "string" then
- size = size + #v + 24
- elseif type(v) == "number" then
- size = size + 8
- elseif type(v) == "boolean" then
- size = size + 4
- elseif type(v) == "table" then
- size = size + estimateTableSize(v, visited, depth + 1)
- elseif type(v) == "function" then
- size = size + 16 -- Function reference
- else
- size = size + 8 -- Other references
- end
- end
-
- return size
-end
-
----Scan StateManager for memory issues
----@return table report Detailed report of StateManager memory usage
-function MemoryScanner.scanStateManager()
- local report = {
- stateCount = 0,
- stateStoreSize = 0,
- metadataSize = 0,
- callSiteCounterSize = 0,
- orphanedStates = {},
- staleStates = {},
- largeStates = {},
- issues = {},
- }
-
- if not MemoryScanner._StateManager then
- table.insert(report.issues, {
- severity = "error",
- message = "StateManager not initialized",
- })
- return report
- end
-
- local internal = MemoryScanner._StateManager._getInternalState()
- local stateStore = internal.stateStore
- local stateMetadata = internal.stateMetadata
- local callSiteCounters = internal.callSiteCounters
- local currentFrame = MemoryScanner._StateManager.getFrameNumber()
-
- -- Count states
- report.stateCount = countTable(stateStore)
-
- -- Estimate sizes
- report.stateStoreSize = estimateTableSize(stateStore)
- report.metadataSize = estimateTableSize(stateMetadata)
- report.callSiteCounterSize = estimateTableSize(callSiteCounters)
-
- -- Check for orphaned states (metadata without state)
- for id, _ in pairs(stateMetadata) do
- if not stateStore[id] then
- table.insert(report.orphanedStates, id)
- end
- end
-
- -- Check for stale states (not accessed in many frames)
- local staleThreshold = 120 -- 2 seconds at 60fps
- for id, meta in pairs(stateMetadata) do
- local framesSinceAccess = currentFrame - meta.lastFrame
- if framesSinceAccess > staleThreshold then
- table.insert(report.staleStates, {
- id = id,
- framesSinceAccess = framesSinceAccess,
- createdFrame = meta.createdFrame,
- accessCount = meta.accessCount,
- })
- end
- end
-
- -- Check for large states (may indicate memory bloat)
- for id, state in pairs(stateStore) do
- local stateSize = estimateTableSize(state)
- if stateSize > 1024 then -- More than 1KB
- table.insert(report.largeStates, {
- id = id,
- size = stateSize,
- keyCount = countTable(state),
- })
- end
- end
-
- -- Check callSiteCounters (should be near 0 after frame cleanup)
- local callSiteCount = countTable(callSiteCounters)
- if callSiteCount > 100 then
- table.insert(report.issues, {
- severity = "warning",
- message = string.format("callSiteCounters has %d entries (expected near 0)", callSiteCount),
- suggestion = "incrementFrame() may not be called properly, or counters aren't being reset",
- })
- end
-
- -- Check for excessive state count
- if report.stateCount > 500 then
- table.insert(report.issues, {
- severity = "warning",
- message = string.format("High state count: %d states", report.stateCount),
- suggestion = "Consider reducing element count or implementing more aggressive cleanup",
- })
- end
-
- -- Check for orphaned states
- if #report.orphanedStates > 0 then
- table.insert(report.issues, {
- severity = "error",
- message = string.format("Found %d orphaned states (metadata without state)", #report.orphanedStates),
- suggestion = "This indicates a bug in state management - metadata should be cleaned up with state",
- })
- end
-
- -- Check for stale states
- if #report.staleStates > 10 then
- table.insert(report.issues, {
- severity = "warning",
- message = string.format("Found %d stale states (not accessed in 2+ seconds)", #report.staleStates),
- suggestion = "Cleanup may not be aggressive enough - consider reducing stateRetentionFrames",
- })
- end
-
- return report
-end
-
----Scan Context for memory issues
----@return table report Detailed report of Context memory usage
-function MemoryScanner.scanContext()
- local report = {
- topElementCount = 0,
- zIndexElementCount = 0,
- frameElementCount = 0,
- issues = {},
- }
-
- if not MemoryScanner._Context then
- table.insert(report.issues, {
- severity = "error",
- message = "Context not initialized",
- })
- return report
- end
-
- -- Count elements
- report.topElementCount = #MemoryScanner._Context.topElements
- report.zIndexElementCount = #MemoryScanner._Context._zIndexOrderedElements
- report.frameElementCount = #MemoryScanner._Context._currentFrameElements
-
- -- Check for stale z-index elements (should be cleared each frame)
- if MemoryScanner._Context.isImmediateMode() then
- -- In immediate mode, _zIndexOrderedElements should be cleared at frame start
- -- If it has elements outside of frame rendering, that's a leak
- if not MemoryScanner._Context._frameStarted and report.zIndexElementCount > 0 then
- table.insert(report.issues, {
- severity = "warning",
- message = string.format("Z-index array has %d elements outside of frame", report.zIndexElementCount),
- suggestion = "clearFrameElements() may not be called properly in beginFrame()",
- })
- end
- end
-
- -- Check for excessive element count
- if report.topElementCount > 100 then
- table.insert(report.issues, {
- severity = "info",
- message = string.format("High top-level element count: %d", report.topElementCount),
- suggestion = "Consider consolidating elements or using fewer top-level containers",
- })
- end
-
- return report
-end
-
----Scan ImageCache for memory issues
----@return table report Detailed report of ImageCache memory usage
-function MemoryScanner.scanImageCache()
- local report = {
- imageCount = 0,
- estimatedMemory = 0,
- issues = {},
- }
-
- if not MemoryScanner._ImageCache then
- table.insert(report.issues, {
- severity = "error",
- message = "ImageCache not initialized",
- })
- return report
- end
-
- local stats = MemoryScanner._ImageCache.getStats()
- report.imageCount = stats.count
- report.estimatedMemory = stats.memoryEstimate
-
- -- Check for excessive memory usage (>100MB)
- if report.estimatedMemory > 100 * 1024 * 1024 then
- table.insert(report.issues, {
- severity = "warning",
- message = string.format("ImageCache using ~%.2f MB", report.estimatedMemory / 1024 / 1024),
- suggestion = "Consider implementing cache eviction or clearing unused images",
- })
- end
-
- -- Check for excessive image count
- if report.imageCount > 50 then
- table.insert(report.issues, {
- severity = "info",
- message = string.format("ImageCache has %d images", report.imageCount),
- suggestion = "Review if all cached images are necessary",
- })
- end
-
- return report
-end
-
----Check if a circular reference is intentional (parent-child, module, or metatable)
----@param path string The current path where circular ref was detected
----@param originalPath string The original path where the table was first seen
----@return boolean True if this is an intentional circular reference
-local function isIntentionalCircularReference(path, originalPath)
- -- Pattern 1: child.parent points back to parent
- -- Example: "topElements.1.children.1.parent" -> "topElements.1"
- if path:match("%.parent$") then
- local parentPath = path:match("^(.+)%.children%.[^.]+%.parent$")
- if parentPath == originalPath then
- return true
- end
- end
-
- -- Pattern 2: parent.children[n] points to child, child points back somewhere in parent tree
- -- Example: "topElements.1" -> "topElements.1.children.1.parent"
- if originalPath:match("%.parent$") then
- local childParentPath = originalPath:match("^(.+)%.children%.[^.]+%.parent$")
- if childParentPath == path then
- return true
- end
- end
-
- -- Pattern 3: Check for nested parent-child cycles
- -- child.children[n].parent -> child
- local segments = {}
- for segment in path:gmatch("[^.]+") do
- table.insert(segments, segment)
- end
-
- -- Look for .children.N.parent pattern
- for i = 1, #segments - 2 do
- if segments[i] == "children" and segments[i + 2] == "parent" then
- -- Reconstruct path without the .children.N.parent suffix
- local reconstructedPath = table.concat(segments, ".", 1, i - 1)
- if reconstructedPath == originalPath then
- return true
- end
- end
- end
-
- -- Pattern 4: Metatable __index self-references (modules)
- -- Example: "element._renderer._Theme.__index" -> "element._renderer._Theme"
- if path:match("%.__index$") then
- local basePath = path:match("^(.+)%.__index$")
- if basePath == originalPath then
- return true
- end
- end
-
- -- Pattern 5: Shared module references (elements sharing same module instances)
- -- Example: Multiple elements referencing _utils, _Theme, _Blur, etc.
- -- These start with _ and are typically modules
- local pathModuleName = path:match("%.(_[%w]+)%.")
- local originalModuleName = originalPath:match("%.(_[%w]+)%.")
-
- if pathModuleName and originalModuleName then
- -- If both paths reference the same internal module (starting with _), it's intentional
- if pathModuleName == originalModuleName then
- return true
- end
- end
-
- -- Pattern 6: Shared Color/Transform objects between elements
- -- These are value objects that can be safely shared
- if path:match("Color") and originalPath:match("Color") then
- return true
- end
- if path:match("Transform") and originalPath:match("Transform") then
- return true
- end
-
- -- Pattern 7: LayoutEngine holding reference to its element
- -- Example: "element._layoutEngine.element" -> "element"
- if path:match("%._layoutEngine%.element$") then
- local elementPath = path:match("^(.+)%._layoutEngine%.element$")
- if elementPath == originalPath then
- return true
- end
- end
-
- -- Pattern 8: Renderer holding references to element properties
- -- Example: "element._renderer.cornerRadius" -> "element.cornerRadius"
- if path:match("%._renderer%.") then
- local rendererBasePath = path:match("^(.+)%._renderer%.")
- local originalBasePath = originalPath:match("^(.+)%.")
- if rendererBasePath == originalBasePath then
- return true
- end
- end
-
- -- Pattern 9: Context reference from layout engine (shared singleton)
- -- Example: "element._layoutEngine._Context.topElements" -> "topElements"
- if path:match("%._layoutEngine%._Context%.") and originalPath == "topElements" then
- return true
- end
-
- return false
-end
-
----Detect circular references in a table
----@param tbl table Table to check
----@param path string? Current path (for reporting)
----@param visited table? Tracking table
----@return table[] circularRefs Array of circular reference paths
----@return table[] intentionalRefs Array of intentional parent-child refs
-local function detectCircularReferences(tbl, path, visited)
- if type(tbl) ~= "table" then
- return {}, {}
- end
-
- path = path or "root"
- visited = visited or {}
- local circularRefs = {}
- local intentionalRefs = {}
-
- -- Check if we've seen this table before
- if visited[tbl] then
- local ref = {
- path = path,
- originalPath = visited[tbl],
- }
-
- -- Determine if this is an intentional circular reference
- if isIntentionalCircularReference(path, visited[tbl]) then
- table.insert(intentionalRefs, ref)
- else
- table.insert(circularRefs, ref)
- end
-
- return circularRefs, intentionalRefs
- end
-
- -- Mark as visited
- visited[tbl] = path
-
- -- Recursively check children
- for k, v in pairs(tbl) do
- if type(v) == "table" then
- local childPath = path .. "." .. tostring(k)
- local childRefs, childIntentionalRefs = detectCircularReferences(v, childPath, visited)
- for _, ref in ipairs(childRefs) do
- table.insert(circularRefs, ref)
- end
- for _, ref in ipairs(childIntentionalRefs) do
- table.insert(intentionalRefs, ref)
- end
- end
- end
-
- return circularRefs, intentionalRefs
-end
-
----Scan for circular references in immediate mode
----@return table report Detailed report of circular references
-function MemoryScanner.scanCircularReferences()
- local report = {
- stateStoreCircularRefs = {},
- stateStoreIntentionalRefs = {},
- contextCircularRefs = {},
- contextIntentionalRefs = {},
- issues = {},
- }
-
- if MemoryScanner._StateManager then
- local internal = MemoryScanner._StateManager._getInternalState()
- report.stateStoreCircularRefs, report.stateStoreIntentionalRefs =
- detectCircularReferences(internal.stateStore, "stateStore")
- end
-
- if MemoryScanner._Context then
- report.contextCircularRefs, report.contextIntentionalRefs =
- detectCircularReferences(MemoryScanner._Context.topElements, "topElements")
- end
-
- -- Report issues only for cross-module circular references
- if #report.stateStoreCircularRefs > 0 then
- table.insert(report.issues, {
- severity = "info",
- message = string.format(
- "Found %d cross-module circular references in StateManager",
- #report.stateStoreCircularRefs
- ),
- suggestion = "These are typically architectural dependencies between modules, not memory leaks",
- })
- end
-
- if #report.contextCircularRefs > 0 then
- table.insert(report.issues, {
- severity = "info",
- message = string.format("Found %d cross-module circular references in Context", #report.contextCircularRefs),
- suggestion = "These are typically architectural dependencies (e.g., layout engine ↔ renderer), not memory leaks",
- })
- end
-
- return report
-end
-
----Run comprehensive memory scan
----@return table report Complete memory analysis report
-function MemoryScanner.scan()
- local startMemory = collectgarbage("count")
-
- local report = {
- timestamp = os.time(),
- startMemory = startMemory / 1024, -- MB
- stateManager = MemoryScanner.scanStateManager(),
- context = MemoryScanner.scanContext(),
- imageCache = MemoryScanner.scanImageCache(),
- circularRefs = MemoryScanner.scanCircularReferences(),
- summary = {
- totalIssues = 0,
- criticalIssues = 0,
- warnings = 0,
- info = 0,
- },
- }
-
- -- Count issues by severity
- local function countIssues(subReport)
- for _, issue in ipairs(subReport.issues or {}) do
- report.summary.totalIssues = report.summary.totalIssues + 1
- if issue.severity == "error" then
- report.summary.criticalIssues = report.summary.criticalIssues + 1
- elseif issue.severity == "warning" then
- report.summary.warnings = report.summary.warnings + 1
- elseif issue.severity == "info" then
- report.summary.info = report.summary.info + 1
- end
- end
- end
-
- countIssues(report.stateManager)
- countIssues(report.context)
- countIssues(report.imageCache)
- countIssues(report.circularRefs)
-
- -- Force GC and measure freed memory
- local beforeGC = collectgarbage("count")
- collectgarbage("collect")
- collectgarbage("collect")
- local afterGC = collectgarbage("count")
-
- report.gcAnalysis = {
- beforeGC = beforeGC / 1024, -- MB
- afterGC = afterGC / 1024, -- MB
- freed = (beforeGC - afterGC) / 1024, -- MB
- freedPercent = ((beforeGC - afterGC) / beforeGC) * 100,
- }
-
- -- Analyze GC effectiveness
- if report.gcAnalysis.freedPercent < 5 then
- table.insert(report.stateManager.issues, {
- severity = "info",
- message = string.format("GC freed only %.1f%% of memory", report.gcAnalysis.freedPercent),
- suggestion = "Most memory is still referenced - this is normal if UI is active",
- })
- elseif report.gcAnalysis.freedPercent > 30 then
- table.insert(report.stateManager.issues, {
- severity = "warning",
- message = string.format("GC freed %.1f%% of memory", report.gcAnalysis.freedPercent),
- suggestion = "Significant memory was unreferenced - may indicate cleanup issues",
- })
- end
-
- return report
-end
-
----Format report as human-readable string
----@param report table Memory scan report
----@return string formatted Formatted report
-function MemoryScanner.formatReport(report)
- local lines = {}
-
- table.insert(lines, "=== FlexLöve Memory Scanner Report ===")
- table.insert(lines, string.format("Timestamp: %s", os.date("%Y-%m-%d %H:%M:%S", report.timestamp)))
- table.insert(lines, string.format("Memory: %.2f MB", report.startMemory))
- table.insert(lines, "")
-
- -- Summary
- table.insert(lines, "--- Summary ---")
- table.insert(lines, string.format("Total Issues: %d", report.summary.totalIssues))
- table.insert(lines, string.format(" Critical: %d", report.summary.criticalIssues))
- table.insert(lines, string.format(" Warnings: %d", report.summary.warnings))
- table.insert(lines, string.format(" Info: %d", report.summary.info))
- table.insert(lines, "")
-
- -- StateManager
- table.insert(lines, "--- StateManager ---")
- table.insert(lines, string.format("State Count: %d", report.stateManager.stateCount))
- table.insert(lines, string.format("State Store Size: %.2f KB", report.stateManager.stateStoreSize / 1024))
- table.insert(lines, string.format("Metadata Size: %.2f KB", report.stateManager.metadataSize / 1024))
- table.insert(lines, string.format("CallSite Counters: %.2f KB", report.stateManager.callSiteCounterSize / 1024))
- table.insert(lines, string.format("Orphaned States: %d", #report.stateManager.orphanedStates))
- table.insert(lines, string.format("Stale States: %d", #report.stateManager.staleStates))
- table.insert(lines, string.format("Large States: %d", #report.stateManager.largeStates))
-
- if #report.stateManager.issues > 0 then
- table.insert(lines, "Issues:")
- for _, issue in ipairs(report.stateManager.issues) do
- table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
- if issue.suggestion then
- table.insert(lines, string.format(" → %s", issue.suggestion))
- end
- end
- end
- table.insert(lines, "")
-
- -- Context
- table.insert(lines, "--- Context ---")
- table.insert(lines, string.format("Top Elements: %d", report.context.topElementCount))
- table.insert(lines, string.format("Z-Index Elements: %d", report.context.zIndexElementCount))
- table.insert(lines, string.format("Frame Elements: %d", report.context.frameElementCount))
-
- if #report.context.issues > 0 then
- table.insert(lines, "Issues:")
- for _, issue in ipairs(report.context.issues) do
- table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
- if issue.suggestion then
- table.insert(lines, string.format(" → %s", issue.suggestion))
- end
- end
- end
- table.insert(lines, "")
-
- -- ImageCache
- table.insert(lines, "--- ImageCache ---")
- table.insert(lines, string.format("Image Count: %d", report.imageCache.imageCount))
- table.insert(lines, string.format("Estimated Memory: %.2f MB", report.imageCache.estimatedMemory / 1024 / 1024))
-
- if #report.imageCache.issues > 0 then
- table.insert(lines, "Issues:")
- for _, issue in ipairs(report.imageCache.issues) do
- table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
- if issue.suggestion then
- table.insert(lines, string.format(" → %s", issue.suggestion))
- end
- end
- end
- table.insert(lines, "")
-
- -- Circular References
- table.insert(lines, "--- Circular References ---")
- table.insert(lines, string.format("StateStore (Cross-module refs): %d", #report.circularRefs.stateStoreCircularRefs))
- table.insert(
- lines,
- string.format(
- "StateStore (Intentional - parent-child, modules, metatables): %d",
- #report.circularRefs.stateStoreIntentionalRefs
- )
- )
- table.insert(lines, string.format("Context (Cross-module refs): %d", #report.circularRefs.contextCircularRefs))
- table.insert(
- lines,
- string.format(
- "Context (Intentional - parent-child, modules, metatables): %d",
- #report.circularRefs.contextIntentionalRefs
- )
- )
-
- if #report.circularRefs.issues > 0 then
- table.insert(lines, "Issues:")
- for _, issue in ipairs(report.circularRefs.issues) do
- table.insert(lines, string.format(" [%s] %s", string.upper(issue.severity), issue.message))
- if issue.suggestion then
- table.insert(lines, string.format(" → %s", issue.suggestion))
- end
- end
- else
- table.insert(lines, " ✓ No unexpected circular references detected")
- end
- table.insert(lines, " Note: Cross-module refs are typically architectural dependencies, not memory leaks")
- table.insert(lines, "")
-
- -- GC Analysis
- table.insert(lines, "--- Garbage Collection Analysis ---")
- table.insert(lines, string.format("Before GC: %.2f MB", report.gcAnalysis.beforeGC))
- table.insert(lines, string.format("After GC: %.2f MB", report.gcAnalysis.afterGC))
- table.insert(lines, string.format("Freed: %.2f MB (%.1f%%)", report.gcAnalysis.freed, report.gcAnalysis.freedPercent))
- table.insert(lines, "")
-
- table.insert(lines, "=== End Report ===")
-
- return table.concat(lines, "\n")
-end
-
----Save report to file
----@param report table Memory scan report
----@param filename string? Output filename (default: memory_report.txt)
-function MemoryScanner.saveReport(report, filename)
- filename = filename or "memory_report.txt"
- local formatted = MemoryScanner.formatReport(report)
-
- local file = io.open(filename, "w")
- if file then
- file:write(formatted)
- file:close()
- if MemoryScanner._ErrorHandler then
- MemoryScanner._ErrorHandler:warn("MemoryScanner", "RES_004", {
- resourceType = "report",
- path = filename,
- status = "saved",
- })
- end
- else
- if MemoryScanner._ErrorHandler then
- MemoryScanner._ErrorHandler:warn("MemoryScanner", "RES_004", {
- resourceType = "report",
- path = filename,
- status = "failed to save",
- })
- end
- end
-end
-
-return MemoryScanner
diff --git a/libs/flexlove/modules/ModuleLoader.lua b/libs/flexlove/modules/ModuleLoader.lua
deleted file mode 100644
index 1b48daf0..00000000
--- a/libs/flexlove/modules/ModuleLoader.lua
+++ /dev/null
@@ -1,202 +0,0 @@
----@class ModuleLoader
-local ModuleLoader = {}
-
--- Module registry to track loaded vs. stub modules
-ModuleLoader._registry = {}
-ModuleLoader._ErrorHandler = nil
-
---- Initialize ModuleLoader with dependencies
----@param deps table
-function ModuleLoader.init(deps)
- ModuleLoader._ErrorHandler = deps.ErrorHandler
-end
-
---- Create a null-object stub for a missing optional module
---- Provides safe defaults that won't cause runtime errors
----@param moduleName string
----@return table
-local function createNullObject(moduleName)
- local stub = {
- _isStub = true,
- _moduleName = moduleName,
- }
-
- -- Common method stubs that return safe defaults
- local metatable = {
- __index = function(_, key)
- -- Common initialization method
- if key == "init" then
- return function()
- return stub
- end
- end
-
- -- Common constructor method
- if key == "new" then
- return function()
- return stub
- end
- end
-
- -- Common draw method
- if key == "draw" then
- return function() end
- end
-
- -- Common update method
- if key == "update" then
- return function() end
- end
-
- -- Common render method
- if key == "render" then
- return function() end
- end
-
- -- Common cleanup method
- if key == "destroy" then
- return function() end
- end
-
- -- Common cleanup method
- if key == "cleanup" then
- return function() end
- end
-
- -- Common clear method
- if key == "clear" then
- return function() end
- end
-
- -- Common reset method
- if key == "reset" then
- return function() end
- end
-
- -- Common get method
- if key == "get" then
- return function()
- return nil
- end
- end
-
- -- Common set method
- if key == "set" then
- return function() end
- end
-
- -- Common load method
- if key == "load" then
- return function()
- return stub
- end
- end
-
- -- Common cache-related methods
- if key == "cache" or key == "getCache" or key == "clearCache" then
- return function()
- return {}
- end
- end
-
- -- For any unknown method, return a no-op function that accepts any arguments
- -- This allows safe method calls on stub objects (e.g., Performance:startFrame())
- return function()
- return stub
- end
- end,
-
- -- Make function calls safe (in case the stub itself is called)
- __call = function()
- return stub
- end,
- }
-
- setmetatable(stub, metatable)
- return stub
-end
-
---- Safely require a module with graceful fallback for optional modules
---- Returns the module if it exists, or a null-object stub if it's optional and missing
---- Throws an error if a required module is missing
----@param modulePath string Full path to the module (e.g., "modules.Performance")
----@param isOptional boolean If true, returns null-object on failure; if false, throws error
----@return table module The loaded module or a null-object stub
-function ModuleLoader.safeRequire(modulePath, isOptional)
- -- Check if already loaded
- if ModuleLoader._registry[modulePath] then
- return ModuleLoader._registry[modulePath]
- end
-
- -- Attempt to load the module
- local success, result = pcall(require, modulePath)
-
- if success then
- -- Module loaded successfully
- ModuleLoader._registry[modulePath] = result
- return result
- else
- -- Module failed to load
- if isOptional then
- -- Create null-object stub for optional module
- local stub = createNullObject(modulePath)
- ModuleLoader._registry[modulePath] = stub
-
- -- Log warning about missing optional module
- if ModuleLoader._ErrorHandler then
- ModuleLoader._ErrorHandler:warn("ModuleLoader", "MOD_001", {
- modulePath = modulePath,
- })
- end
-
- return stub
- else
- -- Required module is missing - throw error
- error(string.format("Required module '%s' not found: %s", modulePath, tostring(result)))
- end
- end
-end
-
---- Check if a module is actually loaded (not a stub)
----@param modulePath string Full path to the module
----@return boolean isLoaded True if module is loaded, false if it's a stub or not loaded
-function ModuleLoader.isModuleLoaded(modulePath)
- local module = ModuleLoader._registry[modulePath]
- if not module then
- return false
- end
-
- -- Check if it's a stub
- return not module._isStub
-end
-
---- Get list of all loaded modules
----@return table modules List of module paths that are actually loaded (not stubs)
-function ModuleLoader.getLoadedModules()
- local loaded = {}
- for path, module in pairs(ModuleLoader._registry) do
- if not module._isStub then
- table.insert(loaded, path)
- end
- end
- return loaded
-end
-
---- Get list of all stub modules
----@return table stubs List of module paths that are stubs
-function ModuleLoader.getStubModules()
- local stubs = {}
- for path, module in pairs(ModuleLoader._registry) do
- if module._isStub then
- table.insert(stubs, path)
- end
- end
- return stubs
-end
-
---- Clear the module registry (useful for testing)
-function ModuleLoader._clearRegistry()
- ModuleLoader._registry = {}
-end
-
-return ModuleLoader
diff --git a/libs/flexlove/modules/NinePatch.lua b/libs/flexlove/modules/NinePatch.lua
deleted file mode 100644
index 4adb9696..00000000
--- a/libs/flexlove/modules/NinePatch.lua
+++ /dev/null
@@ -1,217 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local ImageScaler = require(modulePath .. "ImageScaler")
-
-local NinePatch = {}
-
--- ErrorHandler will be injected via init
-local ErrorHandler = nil
-
---- Initialize NinePatch with dependencies
----@param deps table Dependencies table with ErrorHandler
-function NinePatch.init(deps)
- if deps and deps.ErrorHandler then
- ErrorHandler = deps.ErrorHandler
- end
- -- Also initialize ImageScaler since it's a dependency
- if ImageScaler.init then
- ImageScaler.init(deps)
- end
-end
-
---- Draw a 9-patch component using Android-style rendering
---- Corners are scaled by scaleCorners multiplier, edges stretch in one dimension only
----@param component ThemeComponent
----@param atlas love.Image
----@param x number -- X position (top-left corner)
----@param y number -- Y position (top-left corner)
----@param width number -- Total width (border-box)
----@param height number -- Total height (border-box)
----@param opacity number?
----@param elementScaleCorners number? -- Element-level override for scaleCorners (scale multiplier)
----@param elementScalingAlgorithm "nearest"|"bilinear"? -- Element-level override for scalingAlgorithm
-function NinePatch.draw(component, atlas, x, y, width, height, opacity, elementScaleCorners, elementScalingAlgorithm)
- if not component or not atlas then
- return
- end
-
- opacity = opacity or 1
- love.graphics.setColor(1, 1, 1, opacity)
-
- local regions = component.regions
-
- -- Extract border dimensions from regions (in pixels)
- local left = regions.topLeft.w
- local right = regions.topRight.w
- local top = regions.topLeft.h
- local bottom = regions.bottomLeft.h
- local centerW = regions.middleCenter.w
- local centerH = regions.middleCenter.h
-
- -- Calculate content area (space remaining after borders)
- local contentWidth = width - left - right
- local contentHeight = height - top - bottom
-
- -- Clamp to prevent negative dimensions
- contentWidth = math.max(0, contentWidth)
- contentHeight = math.max(0, contentHeight)
-
- -- Calculate stretch scales for edges and center
- local scaleX = contentWidth / centerW
- local scaleY = contentHeight / centerH
-
- -- Create quads for each region
- local atlasWidth, atlasHeight = atlas:getDimensions()
-
- local function makeQuad(region)
- return love.graphics.newQuad(region.x, region.y, region.w, region.h, atlasWidth, atlasHeight)
- end
-
- -- Get corner scale multiplier
- -- Priority: element-level override > component setting > default (nil = no scaling)
- local scaleCorners = elementScaleCorners
- if scaleCorners == nil then
- scaleCorners = component.scaleCorners
- end
-
- -- Priority: element-level override > component setting > default ("bilinear")
- local scalingAlgorithm = elementScalingAlgorithm
- if scalingAlgorithm == nil then
- scalingAlgorithm = component.scalingAlgorithm or "bilinear"
- end
-
- if scaleCorners and type(scaleCorners) == "number" and scaleCorners > 0 then
- -- Initialize cache if needed
- if not component._scaledRegionCache then
- component._scaledRegionCache = {}
- end
-
- -- Use the numeric scale multiplier directly
- local scaleFactor = scaleCorners
-
- -- Helper to get or create scaled region
- local function getScaledRegion(regionName, region, targetWidth, targetHeight)
- local cacheKey = string.format("%s_%.2f_%s", regionName, scaleFactor, scalingAlgorithm)
-
- if component._scaledRegionCache[cacheKey] then
- return component._scaledRegionCache[cacheKey]
- end
-
- -- Get ImageData from component (stored during theme loading)
- local atlasData = component._loadedAtlasData
- if not atlasData then
- ErrorHandler.error(
- "NinePatch",
- "REN_007",
- "No ImageData available for atlas. Image must be loaded with safeLoadImage.",
- {
- componentType = component.type,
- }
- )
- end
-
- local scaledData
-
- if scalingAlgorithm == "nearest" then
- scaledData =
- ImageScaler.scaleNearest(atlasData, region.x, region.y, region.w, region.h, targetWidth, targetHeight)
- else
- scaledData =
- ImageScaler.scaleBilinear(atlasData, region.x, region.y, region.w, region.h, targetWidth, targetHeight)
- end
-
- -- Convert to image and cache
- local scaledImage = love.graphics.newImage(scaledData)
- component._scaledRegionCache[cacheKey] = scaledImage
-
- return scaledImage
- end
-
- -- Calculate scaled dimensions for corners
- local scaledLeft = math.floor(left * scaleFactor + 0.5)
- local scaledRight = math.floor(right * scaleFactor + 0.5)
- local scaledTop = math.floor(top * scaleFactor + 0.5)
- local scaledBottom = math.floor(bottom * scaleFactor + 0.5)
-
- -- CORNERS (scaled using algorithm)
- local topLeftScaled = getScaledRegion("topLeft", regions.topLeft, scaledLeft, scaledTop)
- local topRightScaled = getScaledRegion("topRight", regions.topRight, scaledRight, scaledTop)
- local bottomLeftScaled = getScaledRegion("bottomLeft", regions.bottomLeft, scaledLeft, scaledBottom)
- local bottomRightScaled = getScaledRegion("bottomRight", regions.bottomRight, scaledRight, scaledBottom)
-
- love.graphics.draw(topLeftScaled, x, y)
- love.graphics.draw(topRightScaled, x + width - scaledRight, y)
- love.graphics.draw(bottomLeftScaled, x, y + height - scaledBottom)
- love.graphics.draw(bottomRightScaled, x + width - scaledRight, y + height - scaledBottom)
-
- -- Update content dimensions to account for scaled borders
- local adjustedContentWidth = width - scaledLeft - scaledRight
- local adjustedContentHeight = height - scaledTop - scaledBottom
- adjustedContentWidth = math.max(0, adjustedContentWidth)
- adjustedContentHeight = math.max(0, adjustedContentHeight)
-
- -- Recalculate stretch scales
- local adjustedScaleX = adjustedContentWidth / centerW
- local adjustedScaleY = adjustedContentHeight / centerH
-
- -- TOP/BOTTOM EDGES (stretch horizontally, scale vertically)
- if adjustedContentWidth > 0 then
- local topCenterScaled = getScaledRegion("topCenter", regions.topCenter, regions.topCenter.w, scaledTop)
- local bottomCenterScaled =
- getScaledRegion("bottomCenter", regions.bottomCenter, regions.bottomCenter.w, scaledBottom)
-
- love.graphics.draw(topCenterScaled, x + scaledLeft, y, 0, adjustedScaleX, 1)
- love.graphics.draw(bottomCenterScaled, x + scaledLeft, y + height - scaledBottom, 0, adjustedScaleX, 1)
- end
-
- -- LEFT/RIGHT EDGES (stretch vertically, scale horizontally)
- if adjustedContentHeight > 0 then
- local middleLeftScaled = getScaledRegion("middleLeft", regions.middleLeft, scaledLeft, regions.middleLeft.h)
- local middleRightScaled = getScaledRegion("middleRight", regions.middleRight, scaledRight, regions.middleRight.h)
-
- love.graphics.draw(middleLeftScaled, x, y + scaledTop, 0, 1, adjustedScaleY)
- love.graphics.draw(middleRightScaled, x + width - scaledRight, y + scaledTop, 0, 1, adjustedScaleY)
- end
-
- -- CENTER (stretch both dimensions, no scaling)
- if adjustedContentWidth > 0 and adjustedContentHeight > 0 then
- love.graphics.draw(
- atlas,
- makeQuad(regions.middleCenter),
- x + scaledLeft,
- y + scaledTop,
- 0,
- adjustedScaleX,
- adjustedScaleY
- )
- end
- else
- -- Original rendering logic (no scaling)
- -- CORNERS (no scaling - 1:1 pixel perfect)
- love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y)
- love.graphics.draw(atlas, makeQuad(regions.topRight), x + left + contentWidth, y)
- love.graphics.draw(atlas, makeQuad(regions.bottomLeft), x, y + top + contentHeight)
- love.graphics.draw(atlas, makeQuad(regions.bottomRight), x + left + contentWidth, y + top + contentHeight)
-
- -- TOP/BOTTOM EDGES (stretch horizontally only)
- if contentWidth > 0 then
- love.graphics.draw(atlas, makeQuad(regions.topCenter), x + left, y, 0, scaleX, 1)
- love.graphics.draw(atlas, makeQuad(regions.bottomCenter), x + left, y + top + contentHeight, 0, scaleX, 1)
- end
-
- -- LEFT/RIGHT EDGES (stretch vertically only)
- if contentHeight > 0 then
- love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + top, 0, 1, scaleY)
- love.graphics.draw(atlas, makeQuad(regions.middleRight), x + left + contentWidth, y + top, 0, 1, scaleY)
- end
-
- -- CENTER (stretch both dimensions)
- if contentWidth > 0 and contentHeight > 0 then
- love.graphics.draw(atlas, makeQuad(regions.middleCenter), x + left, y + top, 0, scaleX, scaleY)
- end
- end
-
- -- Reset color
- love.graphics.setColor(1, 1, 1, 1)
-end
-
-return NinePatch
diff --git a/libs/flexlove/modules/NumberValidation.lua b/libs/flexlove/modules/NumberValidation.lua
deleted file mode 100644
index 94124e35..00000000
--- a/libs/flexlove/modules/NumberValidation.lua
+++ /dev/null
@@ -1,351 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local function req(name)
- return require(modulePath .. name)
-end
-
--- All numeric, range, type, and enum validation lives here.
--- `clamp` is injected via init() to avoid a cross-import into utils.
--- `ErrorHandler` is injected via init() so error reporting routes through
--- the shared handler (matching the pre-split behavior of utils.validate*).
-
-local ErrorHandler = nil
-local clamp = nil
-
---- Initialize dependencies
----@param deps table Dependencies: { ErrorHandler = table, clamp = function }
-local function init(deps)
- if type(deps) == "table" then
- ErrorHandler = deps.ErrorHandler or ErrorHandler
- clamp = deps.clamp or clamp
- end
-end
-
--- Numeric validation utilities
-
---- Check if a value is NaN (not-a-number)
---- @param value any Value to check
---- @return boolean
-local function isNaN(value)
- return type(value) == "number" and value ~= value
-end
-
---- Check if a value is Infinity
---- @param value any Value to check
---- @return boolean
-local function isInfinity(value)
- return type(value) == "number" and (value == math.huge or value == -math.huge)
-end
-
---- Validate a numeric value with comprehensive checks
---- @param value any Value to validate
---- @param options table? Validation options
---- @return boolean, string?, number? Returns valid, errorMessage, sanitizedValue
-local function validateNumber(value, options)
- options = options or {}
-
- -- Check if value is a number type
- if type(value) ~= "number" then
- if options.default ~= nil then
- return true, nil, options.default
- end
- return false, string.format("Value must be a number, got %s", type(value)), nil
- end
-
- -- Check for NaN
- if isNaN(value) then
- if not options.allowNaN then
- if options.default ~= nil then
- return true, nil, options.default
- end
- return false, "Value is NaN (not-a-number)", nil
- end
- end
-
- -- Check for Infinity
- if isInfinity(value) then
- if not options.allowInfinity then
- if options.default ~= nil then
- return true, nil, options.default
- end
- return false, "Value is Infinity", nil
- end
- end
-
- -- Check for integer requirement
- if options.integer and math.floor(value) ~= value then
- return false, string.format("Value must be an integer, got %s", value), nil
- end
-
- -- Check for positive requirement
- if options.positive and value <= 0 then
- return false, string.format("Value must be positive, got %s", value), nil
- end
-
- -- Check bounds
- if options.min and value < options.min then
- return false, string.format("Value %s is below minimum %s", value, options.min), nil
- end
-
- if options.max and value > options.max then
- return false, string.format("Value %s is above maximum %s", value, options.max), nil
- end
-
- return true, nil, value
-end
-
---- Sanitize a numeric value (never errors, always returns valid number)
---- @param value any Value to sanitize
---- @param min number? Minimum value
---- @param max number? Maximum value
---- @param default number? Default value for invalid inputs
---- @return number Sanitized value
-local function sanitizeNumber(value, min, max, default)
- default = default or 0
- min = min or -math.huge
- max = max or math.huge
-
- -- Convert to number if possible
- if type(value) == "string" then
- value = tonumber(value)
- end
-
- -- Handle non-numeric
- if type(value) ~= "number" then
- return default
- end
-
- -- Handle NaN
- if isNaN(value) then
- return default
- end
-
- -- Handle Infinity
- if value == math.huge then
- return max
- end
- if value == -math.huge then
- return min
- end
-
- -- Clamp to range
- return clamp(value, min, max)
-end
-
---- Validate and convert to integer
---- @param value any Value to validate
---- @param min number? Minimum value
---- @param max number? Maximum value
---- @return boolean, string?, number? Returns valid, errorMessage, integerValue
-local function validateInteger(value, min, max)
- local valid, err, sanitized = validateNumber(value, {
- min = min,
- max = max,
- integer = true,
- })
-
- if not valid then
- return false, err, nil
- end
-
- return true, nil, math.floor(sanitized or value)
-end
-
---- Validate and normalize percentage value
---- @param value any Value to validate (can be "50%", 0.5, or 50)
---- @return boolean, string?, number? Returns valid, errorMessage, normalizedValue (0-1)
-local function validatePercentage(value)
- -- Handle string percentage
- if type(value) == "string" then
- local num = value:match("^(%d+%.?%d*)%%$")
- if num then
- value = tonumber(num)
- if value then
- value = value / 100
- end
- else
- value = tonumber(value)
- end
- end
-
- if type(value) ~= "number" then
- return false, "Percentage must be a number", nil
- end
-
- if isNaN(value) or isInfinity(value) then
- return false, "Percentage cannot be NaN or Infinity", nil
- end
-
- -- If value is > 1, assume it's 0-100 range
- if value > 1 then
- value = value / 100
- end
-
- -- Clamp to 0-1
- value = clamp(value, 0, 1)
-
- return true, nil, value
-end
-
---- Validate opacity value (0-1)
---- @param value any Value to validate
---- @return boolean, string?, number? Returns valid, errorMessage, opacityValue
-local function validateOpacity(value)
- return validateNumber(value, { min = 0, max = 1, default = 1 })
-end
-
---- Validate degree value (0-360)
---- @param value any Value to validate
---- @return boolean, string?, number? Returns valid, errorMessage, degreeValue
-local function validateDegrees(value)
- local valid, err, sanitized = validateNumber(value)
- if not valid then
- return false, err, nil
- end
-
- -- Normalize to 0-360 range
- local degrees = sanitized or value
- degrees = degrees % 360
- if degrees < 0 then
- degrees = degrees + 360
- end
-
- return true, nil, degrees
-end
-
---- Validate coordinate value (pixel position)
---- @param value any Value to validate
---- @return boolean, string?, number? Returns valid, errorMessage, coordinateValue
-local function validateCoordinate(value)
- return validateNumber(value, {
- allowNaN = false,
- allowInfinity = false,
- })
-end
-
---- Validate dimension value (width/height, must be non-negative)
---- @param value any Value to validate
---- @return boolean, string?, number? Returns valid, errorMessage, dimensionValue
-local function validateDimension(value)
- return validateNumber(value, {
- min = 0,
- allowNaN = false,
- allowInfinity = false,
- })
-end
-
---- Validate that a value is in an enum table
----@param value any Value to validate
----@param enumTable table Enum table with valid values
----@param propName string Property name for error messages
----@param moduleName string? Module name for error messages (default: "Element")
----@return boolean True if valid
-local function validateEnum(value, enumTable, propName, moduleName)
- if value == nil then
- return true
- end
-
- for _, validValue in pairs(enumTable) do
- if value == validValue then
- return true
- end
- end
-
- -- Build list of valid options
- local validOptions = {}
- for _, v in pairs(enumTable) do
- table.insert(validOptions, "'" .. v .. "'")
- end
- table.sort(validOptions)
-
- if ErrorHandler then
- ErrorHandler:error(moduleName or "Element", "VAL_007", {
- property = propName,
- expected = table.concat(validOptions, ", "),
- got = tostring(value),
- })
- else
- error(
- string.format("%s must be one of: %s. Got: '%s'", propName, table.concat(validOptions, ", "), tostring(value))
- )
- end
-end
-
---- Validate that a numeric value is within a range
----@param value any Value to validate
----@param min number Minimum allowed value
----@param max number Maximum allowed value
----@param propName string Property name for error messages
----@param moduleName string? Module name for error messages (default: "Element")
----@return boolean True if valid
-local function validateRange(value, min, max, propName, moduleName)
- if value == nil then
- return true
- end
- if type(value) ~= "number" then
- if ErrorHandler then
- ErrorHandler:error(moduleName or "Element", "VAL_001", {
- property = propName,
- expected = "number",
- got = type(value),
- })
- else
- error(string.format("%s must be a number, got %s", propName, type(value)))
- end
- elseif value < min or value > max then
- if ErrorHandler then
- ErrorHandler:error(moduleName or "Element", "VAL_002", {
- property = propName,
- min = tostring(min),
- max = tostring(max),
- value = tostring(value),
- })
- else
- error(
- string.format("%s must be between %s and %s, got %s", propName, tostring(min), tostring(max), tostring(value))
- )
- end
- end
- return true
-end
-
---- Validate that a value is of the expected type
----@param value any Value to validate
----@param expectedType string Expected type name
----@param propName string Property name for error messages
----@param moduleName string? Module name for error messages (default: "Element")
----@return boolean True if valid
-local function validateType(value, expectedType, propName, moduleName)
- if value == nil then
- return true
- end
- local actualType = type(value)
- if actualType ~= expectedType then
- if ErrorHandler then
- ErrorHandler:error(moduleName or "Element", "VAL_001", {
- property = propName,
- expected = expectedType,
- got = actualType,
- })
- else
- error(string.format("%s must be %s, got %s", propName, expectedType, actualType))
- end
- end
- return true
-end
-
-return {
- init = init,
- isNaN = isNaN,
- isInfinity = isInfinity,
- validateNumber = validateNumber,
- sanitizeNumber = sanitizeNumber,
- validateInteger = validateInteger,
- validatePercentage = validatePercentage,
- validateOpacity = validateOpacity,
- validateDegrees = validateDegrees,
- validateCoordinate = validateCoordinate,
- validateDimension = validateDimension,
- validateEnum = validateEnum,
- validateRange = validateRange,
- validateType = validateType,
-}
diff --git a/libs/flexlove/modules/PathValidator.lua b/libs/flexlove/modules/PathValidator.lua
deleted file mode 100644
index b3ba0e81..00000000
--- a/libs/flexlove/modules/PathValidator.lua
+++ /dev/null
@@ -1,198 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local function req(name)
- return require(modulePath .. name)
-end
-
--- Path sanitization, validation, and file-extension helpers.
--- Uses love.filesystem when available (optional) for existence checks.
-
---- Normalize a file path for consistent cache keys
----@param path string File path to normalize
----@return string Normalized path
-local function normalizePath(path)
- path = path:match("^%s*(.-)%s*$")
- path = path:gsub("\\", "/")
- path = path:gsub("/+", "/")
- return path
-end
-
---- Sanitize a file path
---- @param path string Path to sanitize
---- @return string Sanitized path
-local function sanitizePath(path)
- if path == nil then
- return ""
- end
- path = tostring(path)
-
- -- Trim whitespace
- path = path:match("^%s*(.-)%s*$") or ""
-
- -- Normalize separators to forward slash
- path = path:gsub("\\", "/")
-
- -- Remove duplicate slashes
- path = path:gsub("/+", "/")
-
- -- Remove trailing slash (except for root)
- if #path > 1 and path:sub(-1) == "/" then
- path = path:sub(1, -2)
- end
-
- return path
-end
-
---- Check if a path is safe (no traversal attacks)
---- @param path string Path to check
---- @param baseDir string? Base directory to check against (optional)
---- @return boolean, string? Returns true if safe, or false with reason
-local function isPathSafe(path, baseDir)
- if path == nil or path == "" then
- return false, "Path is empty"
- end
-
- -- Sanitize the path
- path = sanitizePath(path)
-
- -- Check for suspicious patterns
- if path:match("%.%.") then
- return false, "Path contains '..' (parent directory reference)"
- end
-
- -- Check for null bytes
- if path:match("%z") then
- return false, "Path contains null bytes"
- end
-
- -- Check for encoded traversal attempts (including double-encoding)
- local lowerPath = path:lower()
- if
- lowerPath:match("%%2e")
- or lowerPath:match("%%2f")
- or lowerPath:match("%%5c")
- or lowerPath:match("%%252e")
- or lowerPath:match("%%252f")
- or lowerPath:match("%%255c")
- then
- return false, "Path contains URL-encoded directory separators"
- end
-
- -- If baseDir is provided, ensure path is within it
- if baseDir then
- baseDir = sanitizePath(baseDir)
-
- -- For relative paths, prepend baseDir
- local fullPath = path
- if not path:match("^/") and not path:match("^%a:") then
- fullPath = baseDir .. "/" .. path
- end
- fullPath = sanitizePath(fullPath)
-
- -- Check if fullPath starts with baseDir
- if not fullPath:match("^" .. baseDir:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")) then
- return false, "Path is outside allowed directory"
- end
- end
-
- return true, nil
-end
-
---- Validate a file path with comprehensive checks
---- @param path string Path to validate
---- @param options table? Validation options
---- @return boolean, string? Returns true if valid, or false with error message
-local function validatePath(path, options)
- options = options or {}
-
- -- Check path is not nil/empty
- if path == nil or path == "" then
- return false, "Path is empty"
- end
-
- path = tostring(path)
-
- -- Check maximum length
- local maxLength = options.maxLength or 4096
- if #path > maxLength then
- return false, string.format("Path exceeds maximum length of %d characters", maxLength)
- end
-
- -- Sanitize path
- path = sanitizePath(path)
-
- -- Check for safety (traversal attacks)
- local safe, reason = isPathSafe(path, options.baseDir)
- if not safe then
- return false, reason
- end
-
- -- Check allowed extensions
- if options.allowedExtensions then
- local ext = path:match("%.([^%.]+)$")
- if not ext then
- return false, "Path has no file extension"
- end
-
- ext = ext:lower()
- local allowed = false
- for _, allowedExt in ipairs(options.allowedExtensions) do
- if ext == allowedExt:lower() then
- allowed = true
- break
- end
- end
-
- if not allowed then
- return false, string.format("File extension '%s' is not allowed", ext)
- end
- end
-
- -- Check if file must exist
- if options.mustExist and love and love.filesystem then
- local info = love.filesystem.getInfo(path)
- if not info then
- return false, "File does not exist"
- end
- end
-
- return true, nil
-end
-
---- Get file extension from path
---- @param path string File path
---- @return string? extension File extension (lowercase) or nil
-local function getFileExtension(path)
- if not path then
- return nil
- end
- local ext = path:match("%.([^%.]+)$")
- return ext and ext:lower() or nil
-end
-
---- Check if path has allowed extension
---- @param path string File path
---- @param allowedExtensions table Array of allowed extensions
---- @return boolean
-local function hasAllowedExtension(path, allowedExtensions)
- local ext = getFileExtension(path)
- if not ext then
- return false
- end
-
- for _, allowedExt in ipairs(allowedExtensions) do
- if ext == allowedExt:lower() then
- return true
- end
- end
-
- return false
-end
-
-return {
- normalizePath = normalizePath,
- sanitizePath = sanitizePath,
- isPathSafe = isPathSafe,
- validatePath = validatePath,
- getFileExtension = getFileExtension,
- hasAllowedExtension = hasAllowedExtension,
-}
diff --git a/libs/flexlove/modules/Performance.lua b/libs/flexlove/modules/Performance.lua
deleted file mode 100644
index cf0458f0..00000000
--- a/libs/flexlove/modules/Performance.lua
+++ /dev/null
@@ -1,560 +0,0 @@
----@class Performance
----@field enabled boolean
----@field hudEnabled boolean
----@field hudToggleKey string
----@field hudPosition {x: number, y: number}
----@field warningThresholdMs number
----@field criticalThresholdMs number
----@field logToConsole boolean
----@field logWarnings boolean
----@field warningsEnabled boolean
----@field _ErrorHandler table?
----@field _timers table
----@field _metrics table
----@field _lastMetricsCleanup number
----@field _frameMetrics table
----@field _memoryMetrics table
----@field _warnings table
----@field _lastFrameStart number?
----@field _shownWarnings table
----@field _memoryProfiler table
-local Performance = {}
-Performance.__index = Performance
-
----@type Performance|nil
-local instance = nil
-
-local METRICS_CLEANUP_INTERVAL = 30
-local METRICS_RETENTION_TIME = 10
-local MAX_METRICS_COUNT = 500
-local CORE_METRICS = { frame = true, layout = true, render = true }
-
----@param config {enabled?: boolean, hudEnabled?: boolean, hudToggleKey?: string, hudPosition?: {x: number, y: number}, warningThresholdMs?: number, criticalThresholdMs?: number, logToConsole?: boolean, logWarnings?: boolean, warningsEnabled?: boolean, memoryProfiling?: boolean}?
----@param deps {ErrorHandler: ErrorHandler}
----@return Performance
-function Performance.init(config, deps)
- if instance == nil then
- local self = setmetatable({}, Performance)
-
- -- Configuration
- self.enabled = config and config.enabled or false
- self.hudEnabled = config and config.hudEnabled or false
- self.hudToggleKey = config and config.hudToggleKey or "f3"
- self.hudPosition = config and config.hudPosition or { x = 10, y = 10 }
- self.warningThresholdMs = config and config.warningThresholdMs or 13.0
- self.criticalThresholdMs = config and config.criticalThresholdMs or 16.67
- self.logToConsole = config and config.logToConsole or false
- self.logWarnings = config and config.logWarnings or true
- self.warningsEnabled = config and config.warningsEnabled or true
-
- self._timers = {}
- self._metrics = {}
- self._lastMetricsCleanup = 0
- self._frameMetrics = {
- frameCount = 0,
- totalTime = 0,
- lastFrameTime = 0,
- minFrameTime = math.huge,
- maxFrameTime = 0,
- fps = 0,
- lastFpsUpdate = 0,
- fpsUpdateInterval = 0.5,
- }
- self._memoryMetrics = {
- current = 0,
- peak = 0,
- gcCount = 0,
- lastGcCheck = 0,
- }
- self._warnings = {}
- self._lastFrameStart = nil
- self._shownWarnings = {}
- self._memoryProfiler = {
- enabled = config and config.memoryProfiling or false,
- sampleInterval = 60,
- framesSinceLastSample = 0,
- samples = {},
- maxSamples = 20,
- monitoredTables = {},
- }
- self._ErrorHandler = deps and deps.ErrorHandler
- instance = self
- end
- return instance
-end
-
---- Toggle HUD visibility
-function Performance:toggleHUD()
- self.hudEnabled = not self.hudEnabled
-end
-
-function Performance:startTimer(name)
- if not self.enabled then
- return
- end
- self._timers[name] = love.timer.getTime()
-end
-
-function Performance:stopTimer(name)
- if not self.enabled then
- return nil
- end
-
- local startTime = self._timers[name]
- if not startTime then
- -- Silently return nil if timer wasn't started
- -- This can happen legitimately when Performance is toggled mid-frame
- -- or when layout functions have early returns
- return nil
- end
-
- local elapsed = (love.timer.getTime() - startTime) * 1000
- self._timers[name] = nil
-
- -- Update metrics
- if not self._metrics[name] then
- self._metrics[name] = {
- total = 0,
- count = 0,
- min = math.huge,
- max = 0,
- average = 0,
- lastUsed = love.timer.getTime(),
- }
- end
-
- local m = self._metrics[name]
- m.total = m.total + elapsed
- m.count = m.count + 1
- m.min = math.min(m.min, elapsed)
- m.max = math.max(m.max, elapsed)
- m.average = m.total / m.count
- m.lastUsed = love.timer.getTime()
-
- -- Check for warnings
- if elapsed > self.criticalThresholdMs then
- self:_addWarning(name, elapsed, "critical")
- elseif elapsed > self.warningThresholdMs then
- self:_addWarning(name, elapsed, "warning")
- end
-
- if self.logToConsole then
- -- Use ErrorHandler if available, otherwise fall back to print
- if self._ErrorHandler and self._ErrorHandler.warn then
- self._ErrorHandler:warn("Performance", "PERF_001", {
- metric = name,
- elapsed = string.format("%.3fms", elapsed),
- })
- else
- print(string.format("[Performance] %s: %.3fms", name, elapsed))
- end
- end
-
- return elapsed
-end
-
---- Update with actual delta time from LÖVE (call from love.update)
----@param dt number Delta time in seconds
-function Performance:updateDeltaTime(dt)
- if not self.enabled then
- return
- end
- local now = love.timer.getTime()
- if now - self._frameMetrics.lastFpsUpdate >= self._frameMetrics.fpsUpdateInterval then
- if dt > 0 then
- self._frameMetrics.fps = math.floor(1 / dt + 0.5)
- end
- self._frameMetrics.lastFpsUpdate = now
- end
-end
-
---- Start frame timing (call at beginning of frame)
-function Performance:startFrame()
- if not self.enabled then
- return
- end
- self._lastFrameStart = love.timer.getTime()
- self:_updateMemory()
-end
-
-function Performance:endFrame()
- if not self.enabled or not self._lastFrameStart then
- return
- end
-
- local now = love.timer.getTime()
- local frameTime = (now - self._lastFrameStart) * 1000
-
- self._frameMetrics.lastFrameTime = frameTime
- self._frameMetrics.totalTime = self._frameMetrics.totalTime + frameTime
- self._frameMetrics.frameCount = self._frameMetrics.frameCount + 1
- self._frameMetrics.minFrameTime = math.min(self._frameMetrics.minFrameTime, frameTime)
- self._frameMetrics.maxFrameTime = math.max(self._frameMetrics.maxFrameTime, frameTime)
-
- if frameTime > self.criticalThresholdMs then
- self:_addWarning("frame", frameTime, "critical")
- end
-
- self:updateMemoryProfiling()
-
- -- Periodic metrics cleanup
- if now - self._lastMetricsCleanup >= METRICS_CLEANUP_INTERVAL then
- local cleanupTime = now - METRICS_RETENTION_TIME
- for name, data in pairs(self._metrics) do
- if not CORE_METRICS[name] and data.lastUsed and data.lastUsed < cleanupTime then
- self._metrics[name] = nil
- end
- end
- self._lastMetricsCleanup = now
- end
-
- -- Enforce max metrics limit
- local metricsCount = 0
- for _ in pairs(self._metrics) do
- metricsCount = metricsCount + 1
- end
-
- if metricsCount > MAX_METRICS_COUNT then
- local sortedMetrics = {}
- for name, data in pairs(self._metrics) do
- if not CORE_METRICS[name] then
- table.insert(sortedMetrics, { name = name, lastUsed = data.lastUsed or 0 })
- end
- end
-
- table.sort(sortedMetrics, function(a, b)
- return a.lastUsed < b.lastUsed
- end)
-
- local toRemove = metricsCount - MAX_METRICS_COUNT
- for i = 1, math.min(toRemove, #sortedMetrics) do
- self._metrics[sortedMetrics[i].name] = nil
- end
- end
-end
-
---- Update memory metrics
-function Performance:_updateMemory()
- if not self.enabled then
- return
- end
-
- local memKb = collectgarbage("count")
- self._memoryMetrics.current = memKb
- self._memoryMetrics.peak = math.max(self._memoryMetrics.peak, memKb)
-
- local now = love.timer.getTime()
- if now - self._memoryMetrics.lastGcCheck >= 1.0 then
- self._memoryMetrics.gcCount = self._memoryMetrics.gcCount + 1
- self._memoryMetrics.lastGcCheck = now
- end
-end
-
---- Add a performance warning (private)
---- @param name string Metric name
---- @param value number Metric value
---- @param level "warning"|"critical" Warning level
-function Performance:_addWarning(name, value, level)
- if not self.logWarnings then
- return
- end
-
- local warning = {
- name = name,
- value = value,
- level = level,
- time = love.timer.getTime(),
- }
-
- table.insert(self._warnings, warning)
-
- if #self._warnings > 100 then
- table.remove(self._warnings, 1)
- end
-
- if self.logToConsole or self.warningsEnabled then
- local warningKey = name .. "_" .. level
- local lastWarningTime = self._shownWarnings[warningKey] or 0
- local now = love.timer.getTime()
-
- if now - lastWarningTime >= 60 then
- if self._ErrorHandler and self._ErrorHandler.warn then
- local code = level == "critical" and "PERF_002" or "PERF_001"
-
- self._ErrorHandler:warn("Performance", code, {
- metric = name,
- value = string.format("%.2fms", value),
- threshold = level == "critical" and self.criticalThresholdMs or self.warningThresholdMs,
- })
- end
-
- self._shownWarnings[warningKey] = now
- end
- end
-end
-
---- Render performance HUD
---- @param x number? X position (default: 10)
---- @param y number? Y position (default: 10)
-function Performance:renderHUD(x, y)
- if not self.hudEnabled then
- return
- end
-
- x = x or self.hudPosition.x
- y = y or self.hudPosition.y
-
- self:_updateMemory()
-
- local fm = self._frameMetrics
- local mm = self._memoryMetrics
-
- love.graphics.setColor(0, 0, 0, 0.8)
- love.graphics.rectangle("fill", x, y, 300, 220)
-
- love.graphics.setColor(1, 1, 1, 1)
- local lineHeight = 18
- local currentY = y + 10
-
- -- FPS
- local fpsColor = { 1, 1, 1 }
- if fm.lastFrameTime > self.criticalThresholdMs then
- fpsColor = { 1, 0, 0 }
- elseif fm.lastFrameTime > self.warningThresholdMs then
- fpsColor = { 1, 1, 0 }
- end
- love.graphics.setColor(fpsColor)
- love.graphics.print(string.format("FPS: %d (%.2fms)", fm.fps, fm.lastFrameTime), x + 10, currentY)
- currentY = currentY + lineHeight
-
- love.graphics.setColor(1, 1, 1, 1)
- local avgFrame = fm.frameCount > 0 and fm.totalTime / fm.frameCount or 0
- love.graphics.print(string.format("Avg Frame: %.2fms", avgFrame), x + 10, currentY)
- currentY = currentY + lineHeight
- love.graphics.print(string.format("Min/Max: %.2f/%.2fms", fm.minFrameTime, fm.maxFrameTime), x + 10, currentY)
- currentY = currentY + lineHeight
-
- local currentMb = mm.current / 1024
- local peakMb = mm.peak / 1024
- love.graphics.print(string.format("Memory: %.2f MB (peak: %.2f MB)", currentMb, peakMb), x + 10, currentY)
- currentY = currentY + lineHeight
-
- local metricsCount = 0
- for _ in pairs(self._metrics) do
- metricsCount = metricsCount + 1
- end
- local metricsColor = metricsCount > MAX_METRICS_COUNT * 0.8 and { 1, 0.5, 0 } or { 1, 1, 1 }
- love.graphics.setColor(metricsColor)
- love.graphics.print(string.format("Metrics: %d/%d", metricsCount, MAX_METRICS_COUNT), x + 10, currentY)
- currentY = currentY + lineHeight + 5
-
- -- Top timings
- love.graphics.setColor(1, 1, 1, 1)
- local sortedMetrics = {}
- for name, data in pairs(self._metrics) do
- table.insert(sortedMetrics, { name = name, average = data.average })
- end
- table.sort(sortedMetrics, function(a, b)
- return a.average > b.average
- end)
-
- love.graphics.print("Top Timings:", x + 10, currentY)
- currentY = currentY + lineHeight
-
- for i = 1, math.min(5, #sortedMetrics) do
- local m = sortedMetrics[i]
- love.graphics.print(string.format(" %s: %.3fms", m.name, m.average), x + 10, currentY)
- currentY = currentY + lineHeight
- end
-
- if #self._warnings > 0 then
- love.graphics.setColor(1, 0.5, 0, 1)
- love.graphics.print(string.format("Warnings: %d", #self._warnings), x + 10, currentY)
- end
-end
-
---- Handle keyboard input for HUD toggle
---- @param key string Key pressed
-function Performance:keypressed(key)
- if key == self.hudToggleKey then
- self:toggleHUD()
- end
-end
-
---- Log a performance warning (only once per warning key)
---- @param warningKey string Unique key for this warning type
---- @param module string Module name (e.g., "LayoutEngine", "Element")
---- @param message string Warning message
---- @param details table? Additional details
---- @param suggestion string? Optimization suggestion
-function Performance:logWarning(warningKey, module, message, details, suggestion)
- if not self.warningsEnabled then
- return
- end
-
- if self._shownWarnings[warningKey] then
- return
- end
-
- self._shownWarnings[warningKey] = true
-
- local count = 0
- for _ in pairs(self._shownWarnings) do
- count = count + 1
- end
- if count > 1000 then
- self._shownWarnings = { [warningKey] = true }
- end
-
- if self._ErrorHandler and self._ErrorHandler.warn then
- self._ErrorHandler:warn(module, "PERF_001", details or {})
- end
-end
-
---- Track a counter metric (increments per frame)
---- @param name string Counter name
---- @param value number? Value to add (default: 1)
-function Performance:incrementCounter(name, value)
- if not self.enabled then
- return
- end
-
- value = value or 1
-
- if not self._metrics[name] then
- self._metrics[name] = {
- total = 0,
- count = 0,
- min = math.huge,
- max = 0,
- average = 0,
- frameValue = 0,
- lastUsed = love.timer.getTime(),
- }
- end
-
- local m = self._metrics[name]
- m.frameValue = (m.frameValue or 0) + value
- m.lastUsed = love.timer.getTime()
-end
-
---- Reset frame counters (call at end of frame)
-function Performance:resetFrameCounters()
- if not self.enabled then
- return
- end
-
- local now = love.timer.getTime()
- local toRemove = {}
-
- for name, data in pairs(self._metrics) do
- if data.frameValue then
- if data.frameValue > 0 then
- data.total = data.total + data.frameValue
- data.count = data.count + 1
- data.min = math.min(data.min, data.frameValue)
- data.max = math.max(data.max, data.frameValue)
- data.average = data.total / data.count
- data.lastUsed = now
- end
-
- data.frameValue = 0
-
- if data.count == 0 and not CORE_METRICS[name] then
- table.insert(toRemove, name)
- end
- end
- end
-
- for _, name in ipairs(toRemove) do
- self._metrics[name] = nil
- end
-end
-
---- Register a table for memory leak monitoring
---- @param name string Friendly name for the table
---- @param tableRef table Reference to the table to monitor
-function Performance:registerTableForMonitoring(name, tableRef)
- self._memoryProfiler.monitoredTables[name] = tableRef
-end
-
-function Performance:_sampleMemory()
- local sample = {
- time = love.timer.getTime(),
- memory = collectgarbage("count") / 1024, -- MB
- tableSizes = {},
- }
- local function getTableSize(tbl)
- local count = 0
- for _ in pairs(tbl) do
- count = count + 1
- end
- return count
- end
-
- for name, tableRef in pairs(self._memoryProfiler.monitoredTables) do
- sample.tableSizes[name] = getTableSize(tableRef)
- end
-
- table.insert(self._memoryProfiler.samples, sample)
-
- -- Keep only maxSamples
- if #self._memoryProfiler.samples > self._memoryProfiler.maxSamples then
- table.remove(self._memoryProfiler.samples, 1)
- end
-
- -- Check for memory leaks (consistent growth)
- if #self._memoryProfiler.samples >= 5 then
- for name, _ in pairs(self._memoryProfiler.monitoredTables) do
- local sizes = {}
- for i = math.max(1, #self._memoryProfiler.samples - 4), #self._memoryProfiler.samples do
- table.insert(sizes, self._memoryProfiler.samples[i].tableSizes[name])
- end
-
- -- Check if table is consistently growing
- local growing = true
- for i = 2, #sizes do
- if sizes[i] <= sizes[i - 1] then
- growing = false
- break
- end
- end
-
- if growing and sizes[#sizes] > sizes[1] * 1.5 then
- self:_addWarning("memory_leak", sizes[#sizes], "warning")
-
- if not self._shownWarnings[name] then
- local message = string.format("Table '%s' growing consistently", name)
- if self._ErrorHandler and self._ErrorHandler.warn then
- self._ErrorHandler:warn("Performance", "MEM_001", {
- table = name,
- initialSize = sizes[1],
- currentSize = sizes[#sizes],
- growthPercent = math.floor(((sizes[#sizes] / sizes[1]) - 1) * 100),
- })
- end
-
- self._shownWarnings[name] = true
- end
- elseif not growing then
- self._shownWarnings[name] = nil
- end
- end
- end
-end
-
---- Update memory profiling (call from endFrame)
-function Performance:updateMemoryProfiling()
- if not self._memoryProfiler.enabled then
- return
- end
-
- self._memoryProfiler.framesSinceLastSample = self._memoryProfiler.framesSinceLastSample + 1
-
- if self._memoryProfiler.framesSinceLastSample >= self._memoryProfiler.sampleInterval then
- self:_sampleMemory()
- self._memoryProfiler.framesSinceLastSample = 0
- end
-end
-
-return Performance
diff --git a/libs/flexlove/modules/PropertySchema.lua b/libs/flexlove/modules/PropertySchema.lua
deleted file mode 100644
index d2fe5b43..00000000
--- a/libs/flexlove/modules/PropertySchema.lua
+++ /dev/null
@@ -1,505 +0,0 @@
--- modules/PropertySchema.lua
---
--- Declarative source of truth for every Element prop.
---
--- Each entry describes one prop that Element.new / Element:setProperty currently
--- handles inline. Downstream tasks (03 data-driven prop binding, 05 registry-driven
--- setProperty dispatch) read this metadata instead of hardcoding property names.
---
--- Design constraints (locked — tasks 03/05 depend on this API):
--- * Pure Lua — NO `love` import, NO dependency on utils/Color/Units/ErrorHandler.
--- Normalizers/validators are small, dependency-free closures so the module is
--- unit-testable standalone. Color/^/unit/enum *defaults* that require those
--- modules are left as `nil` here and applied by construction-time special
--- handlers in Task 03; only defaults expressible as literals are stored.
--- * O(1) lookup — `get(name)` is a single table index into a pre-built registry;
--- no per-call construction.
--- * Additive — `define(specs)` merges entries by name so build profiles can
--- extend/override without rebuilding the whole table.
---
--- Metadata shape per prop (all fields present, false/nil when not applicable):
--- type string — type tag for tooling ("number"|"string"|"boolean"|
--- "table"|"function"|"color"|"any")
--- default any|nil — literal default value applied when prop is absent
--- normalizer fn|nil — pure fn(value) -> value; transforms input before
--- storage (e.g. single-value padding -> 4-side table)
--- validator fn|nil — pure fn(value) -> bool; returns false for invalid
--- input (Task 03 warns + falls back on false)
--- isDimension boolean — true for width/height: setProperty routes these
--- through _resolveDimensionProperty (unit-string
--- resolution + border-box sync). Other unit-accepting
--- props (x/y/gap/padding/etc.) are resolved at
--- construction via special handlers, NOT via this flag.
--- affectsLayout boolean — true for props in the legacy setProperty
--- `layoutProperties` table; setting one invalidates
--- layout (matches baseline behavior exactly).
--- syncsTheme boolean — true for props whose setProperty path must reach
--- ThemeManager/Renderer (disabled/active/themeComponent)
--- hasDeferred boolean — true for callbacks that have an `onDeferred`
--- boolean companion prop (auto-wired by Task 03)
--- storageKey string|nil— when set, the prop is stored on the element under
--- this key instead of its own name (prop aliases, e.g.
--- isDisabled -> stored as `disabled`)
-
-local PropertySchema = {}
-
----@type table
-local registry = {}
-
--- ---------------------------------------------------------------------------
--- Pure normalizers (small + dependency-free; hot-pathed during construction)
--- ---------------------------------------------------------------------------
-
---- Expand a single value to a 4-side table. Leaves tables unchanged. nil passthrough.
---- Used by padding/margin: `padding = 5` -> `{top=5,right=5,bottom=5,left=5}`.
-local function expandSides(value)
- if value == nil then
- return nil
- end
- if type(value) == "table" then
- return value
- end
- return { top = value, right = value, bottom = value, left = value }
-end
-
---- Normalize flex direction aliases to internal enum names.
---- "row" -> "horizontal", "column" -> "vertical",
---- "row-reverse" -> "horizontal-reverse", "column-reverse" -> "vertical-reverse";
---- everything else passes through.
-local function normalizeFlexDirection(value)
- if value == "row" then
- return "horizontal"
- elseif value == "column" then
- return "vertical"
- elseif value == "row-reverse" then
- return "horizontal-reverse"
- elseif value == "column-reverse" then
- return "vertical-reverse"
- end
- return value
-end
-
---- Replicate Element.new's border-shape normalization (pure).
---- * table with sides: true -> 1, number -> value, false/nil -> false; nil if no
---- truthy side remains.
---- * number / other truthy scalar: kept as-is.
---- * nil / false: nil.
-local function normalizeBorder(value)
- if value == nil or value == false then
- return nil
- end
- if type(value) == "table" then
- local function side(v)
- if v == true then
- return 1
- elseif type(v) == "number" then
- return v
- else
- return false
- end
- end
- local t = side(value.top)
- local r = side(value.right)
- local b = side(value.bottom)
- local l = side(value.left)
- if not (t or r or b or l) then
- return nil
- end
- return { top = t, right = r, bottom = b, left = l }
- end
- return value
-end
-
---- Replicate Element.new's cornerRadius-shape normalization (pure).
---- * number: 0 -> nil, else the number.
---- * table: nil if all four sides are zero/absent, else fill zeros for absent sides.
---- * nil -> nil.
-local function normalizeCornerRadius(value)
- if value == nil then
- return nil
- end
- if type(value) == "number" then
- if value == 0 then
- return nil
- end
- return value
- end
- if type(value) == "table" then
- -- Mirrors Element.new: `or` truthiness (0 is truthy in Lua). Only an all-
- -- nil/false table collapses to nil; any present side — including 0 — yields
- -- the 4-side table with zero-filled absent sides.
- local hasAny = value.topLeft or value.topRight or value.bottomLeft or value.bottomRight
- if not hasAny then
- return nil
- end
- return {
- topLeft = value.topLeft or 0,
- topRight = value.topRight or 0,
- bottomLeft = value.bottomLeft or 0,
- bottomRight = value.bottomRight or 0,
- }
- end
- return value
-end
-
--- ---------------------------------------------------------------------------
--- Pure validators (dependency-free; return boolean)
--- ---------------------------------------------------------------------------
-
---- Range validator factory: returns fn(v) -> bool. nil is treated as valid
---- (absence handling is the default mechanism's job).
-local function rangeValidator(min, max)
- return function(v)
- if v == nil then
- return true
- end
- return type(v) == "number" and v >= min and v <= max
- end
-end
-
---- Enum validator factory: returns fn(v) -> bool for membership in `set` (set may
---- be an array or a map of value->truthy).
-local function enumValidator(set)
- local lookup = {}
- if type(set) == "table" then
- for k, v in pairs(set) do
- if type(k) == "number" then
- lookup[v] = true
- else
- lookup[k] = true
- end
- end
- end
- return function(v)
- if v == nil then
- return true
- end
- return lookup[v] == true
- end
-end
-
---- Boolean validator: nil is valid (absence); otherwise must be a boolean.
-local function booleanValidator(v)
- return v == nil or type(v) == "boolean"
-end
-
--- ---------------------------------------------------------------------------
--- Registry construction
--- ---------------------------------------------------------------------------
-
---- Build a fully-populated metadata entry, filling omitted fields with defaults.
-local function entry(spec)
- return {
- type = spec.type or "any",
- default = spec.default,
- normalizer = spec.normalizer,
- validator = spec.validator,
- isDimension = spec.isDimension == true,
- affectsLayout = spec.affectsLayout == true,
- syncsTheme = spec.syncsTheme == true,
- hasDeferred = spec.hasDeferred == true,
- storageKey = spec.storageKey,
- }
-end
-
---- Merge prop specs into the registry (additive; later entries override earlier).
----@param specs table map of prop-name -> spec
----@return table registry the live registry table (for chaining/inspection)
-function PropertySchema.define(specs)
- for name, spec in pairs(specs) do
- registry[name] = entry(spec)
- end
- return registry
-end
-
---- O(1) metadata lookup.
----@param name string prop name
----@return table|nil metadata nil for unknown props (no error)
-function PropertySchema.get(name)
- return registry[name]
-end
-
---- Return the live registry (for inspection / coverage assertions only — not for
---- per-call construction).
----@return table
-function PropertySchema.all()
- return registry
-end
-
---- True if a prop is registered.
----@param name string
----@return boolean
-function PropertySchema.has(name)
- return registry[name] ~= nil
-end
-
---- True if setting this prop invalidates layout (legacy `layoutProperties` set).
---- O(1) registry lookup — no per-call table construction. Unknown props return false,
---- matching the legacy `layoutProperties[name]` nil-lookup behavior exactly.
----@param name string prop name
----@return boolean
-function PropertySchema.affectsLayout(name)
- local meta = registry[name]
- return meta ~= nil and meta.affectsLayout == true
-end
-
---- True for dimension props (width/height) that `setProperty` routes through
---- `_resolveDimensionProperty` (unit-string resolution + border-box sync).
---- O(1) registry lookup — no per-call table construction. Unknown props return false,
---- matching the legacy `dimensionProperties[name]` nil-lookup behavior exactly.
----@param name string prop name
----@return boolean
-function PropertySchema.isDimension(name)
- local meta = registry[name]
- return meta ~= nil and meta.isDimension == true
-end
-
---- True for props whose setProperty path must reach ThemeManager/Renderer
---- (disabled/active/themeComponent). O(1) registry lookup — no per-call table
---- construction. Unknown props return false, matching a legacy nil-lookup exactly.
----@param name string prop name
----@return boolean
-function PropertySchema.syncsTheme(name)
- local meta = registry[name]
- return meta ~= nil and meta.syncsTheme == true
-end
-
--- ---------------------------------------------------------------------------
--- Default schema (covers every prop handled in Element.new lines 259-1909 and
--- Element:setProperty lines 4291-4417 of the Task-01 baseline).
--- ---------------------------------------------------------------------------
-local function defineDefaults()
- PropertySchema.define({
- -- ------------------------------------------------------------------ identity
- id = { type = "string" },
- userdata = { type = "any" },
- parent = { type = "table", affectsLayout = true },
- children = { type = "table" },
-
- -- ------------------------------------------------------------------ callbacks
- onEvent = { type = "function", hasDeferred = true },
- onFocus = { type = "function", hasDeferred = true },
- onBlur = { type = "function", hasDeferred = true },
- onTextInput = { type = "function", hasDeferred = true },
- onTextChange = { type = "function", hasDeferred = true },
- onEnter = { type = "function", hasDeferred = true },
- onCreate = { type = "function", hasDeferred = true },
- onTouchEvent = { type = "function", hasDeferred = true },
- onGesture = { type = "function", hasDeferred = true },
- onImageLoad = { type = "function", hasDeferred = true },
- onImageError = { type = "function", hasDeferred = true },
-
- -- Deferred companion flags (stored directly; no further Deferred companion)
- onEventDeferred = { type = "boolean", default = false },
- onFocusDeferred = { type = "boolean", default = false },
- onBlurDeferred = { type = "boolean", default = false },
- onTextInputDeferred = { type = "boolean", default = false },
- onTextChangeDeferred = { type = "boolean", default = false },
- onEnterDeferred = { type = "boolean", default = false },
- onCreateDeferred = { type = "boolean", default = false },
- onTouchEventDeferred = { type = "boolean", default = false },
- onGestureDeferred = { type = "boolean", default = false },
- onImageLoadDeferred = { type = "boolean", default = false },
- onImageErrorDeferred = { type = "boolean", default = false },
-
- -- focus / touch behavior
- dropFocusOnSelection = { type = "boolean" },
- customDraw = { type = "function" },
- touchEnabled = { type = "boolean", default = true },
- multiTouchEnabled = { type = "boolean", default = false },
-
- -- ------------------------------------------------------------------ theme
- theme = { type = "table" },
- themeComponent = { type = "string", syncsTheme = true },
- disabled = { type = "boolean", default = false, syncsTheme = true },
- isDisabled = {
- type = "boolean",
- default = false,
- syncsTheme = true,
- storageKey = "disabled",
- },
- active = { type = "boolean", default = false, syncsTheme = true },
- disableHighlight = { type = "boolean" },
- themeStateLock = { type = "boolean" },
- themeComponentDisabledStates = { type = "table" },
- scaleCorners = { type = "boolean" },
- scalingAlgorithm = { type = "string" },
- contentAutoSizingMultiplier = { type = "table" },
- contentBlur = { type = "table" },
- backdropBlur = { type = "table" },
-
- -- ------------------------------------------------------------------ text editing
- editable = { type = "boolean", default = false },
- multiline = { type = "boolean", default = false },
- passwordMode = { type = "boolean", default = false },
- textWrap = { type = "string" }, -- default computed from multiline
- maxLines = { type = "number" },
- maxLength = { type = "number" },
- placeholder = { type = "string" },
- inputType = { type = "string", default = "text" },
- textOverflow = { type = "string", default = "clip" },
- scrollable = { type = "boolean" }, -- default = multiline
- autoGrow = { type = "boolean" }, -- default = multiline
- selectOnFocus = { type = "boolean", default = false },
- cursorColor = { type = "color" },
- selectionColor = { type = "color" },
- cursorBlinkRate = { type = "number", default = 0.5 },
- text = { type = "string" },
- textAlign = {
- type = "string",
- default = "start",
- validator = enumValidator({ "start", "center", "end", "justify" }),
- },
- -- textAlignVertical is a derived storage field split out from textAlign
- -- (bindVisualState resolves table/compound-string input into H + V). Its
- -- validator is exposed for bindVisualState to validate the V component; the
- -- prop itself stays in SPECIAL_PROPS because compound parsing needs
- -- ErrorHandler warnings (schema is pure-Lua, cannot warn).
- textAlignVertical = {
- type = "string",
- default = "start",
- validator = enumValidator({ "start", "center", "end" }),
- },
- textColor = { type = "color" },
- fontFamily = { type = "string" },
- textSize = { type = "any" }, -- number | preset string; resolved by special handler
- minTextSize = { type = "number" },
- maxTextSize = { type = "number" },
- autoScaleText = { type = "boolean", default = true },
-
- -- ------------------------------------------------------------------ dimensions / box model
- width = { type = "any", isDimension = true, affectsLayout = true },
- height = { type = "any", isDimension = true, affectsLayout = true },
- x = { type = "any", affectsLayout = false },
- y = { type = "any", affectsLayout = false },
- minWidth = { type = "any" },
- maxWidth = { type = "any" },
- minHeight = { type = "any" },
- maxHeight = { type = "any" },
- gap = { type = "any", affectsLayout = true },
- padding = {
- type = "any",
- affectsLayout = true,
- normalizer = expandSides,
- },
- margin = {
- type = "any",
- affectsLayout = true,
- normalizer = expandSides,
- },
- flexDirection = {
- type = "string",
- default = "horizontal",
- affectsLayout = true,
- normalizer = normalizeFlexDirection,
- },
- flexWrap = { type = "string", default = "nowrap", affectsLayout = true },
- justifyContent = { type = "string", default = "flex-start", affectsLayout = true },
- alignItems = { type = "string", default = "stretch", affectsLayout = true },
- alignContent = { type = "string", default = "stretch", affectsLayout = true },
- positioning = { type = "string", default = "relative", affectsLayout = true },
- gridRows = { type = "number", affectsLayout = true },
- gridColumns = { type = "number", affectsLayout = true },
- top = { type = "any", affectsLayout = true },
- right = { type = "any", affectsLayout = true },
- bottom = { type = "any", affectsLayout = true },
- left = { type = "any", affectsLayout = true },
- columnGap = { type = "any" },
- rowGap = { type = "any" },
- flex = { type = "any" }, -- shorthand: expands to flexGrow/flexShrink/flexBasis
- flexGrow = { type = "number", default = 0, validator = rangeValidator(0, math.huge) },
- flexShrink = { type = "number", default = 1, validator = rangeValidator(0, math.huge) },
- flexBasis = { type = "any", default = "auto" },
- alignSelf = { type = "string", default = "auto" },
- justifySelf = { type = "string" },
- z = { type = "number", default = 0 },
- tabIndex = { type = "number" },
-
- -- ------------------------------------------------------------------ border / background / visual
- border = { type = "any", normalizer = normalizeBorder },
- borderColor = { type = "color" }, -- default Color.new(0,0,0,1) via special handler
- backgroundColor = { type = "color" }, -- default transparent via special handler
- opacity = {
- type = "number",
- default = 1,
- validator = rangeValidator(0, 1),
- },
- visibility = { type = "string", default = "visible" },
- display = {
- type = "boolean",
- default = true,
- validator = booleanValidator,
- },
- transform = { type = "table" },
- cornerRadius = { type = "any", normalizer = normalizeCornerRadius },
-
- -- ------------------------------------------------------------------ image
- imagePath = { type = "string" },
- image = { type = "table" },
- objectFit = {
- type = "string",
- default = "fill",
- validator = enumValidator({ "fill", "contain", "cover", "scale-down", "none" }),
- },
- objectPosition = { type = "string", default = "center center" },
- imageOpacity = {
- type = "number",
- default = 1,
- validator = rangeValidator(0, 1),
- },
- imageRepeat = {
- type = "string",
- default = "no-repeat",
- validator = enumValidator({
- "no-repeat",
- "repeat",
- "repeat-x",
- "repeat-y",
- "space",
- "round",
- }),
- },
- imageTint = { type = "color" },
-
- -- ------------------------------------------------------------------ scroll / scrollbar
- overflow = { type = "string" },
- overflowX = { type = "string" },
- overflowY = { type = "string" },
- scrollbarWidth = { type = "number" },
- scrollbarColor = { type = "color" },
- scrollbarTrackColor = { type = "color" },
- scrollbarRadius = { type = "number" },
- scrollbarPadding = { type = "number" },
- scrollSpeed = { type = "number" },
- invertScroll = { type = "boolean" },
- smoothScrollEnabled = { type = "boolean" },
- scrollBarStyle = { type = "string" },
- scrollbarKnobOffset = { type = "number" },
- hideScrollbars = { type = "boolean" },
- scrollbarPlacement = { type = "string" },
- scrollbarBalance = { type = "number" },
- _scrollX = { type = "number", storageKey = "_scrollX" },
- _scrollY = { type = "number", storageKey = "_scrollY" },
-
- -- ------------------------------------------------------------------ select
- selectParent = { type = "table" },
- selectOption = { type = "table" },
-
- -- ------------------------------------------------------------------ transition
- transition = { type = "table", default = {} },
- })
-end
-
---- (Re)populate the default schema. Idempotent: safe to call from Element.init
---- for build profiles that re-require the module. Returns the live registry.
----@return table registry
-function PropertySchema.populate()
- defineDefaults()
- return registry
-end
-
--- Auto-populate on require so the registry is ready without an explicit init call
--- (pure module, no external deps — safe at load time).
-PropertySchema.populate()
-
-return PropertySchema
diff --git a/libs/flexlove/modules/Renderer.lua b/libs/flexlove/modules/Renderer.lua
deleted file mode 100644
index 8265793e..00000000
--- a/libs/flexlove/modules/Renderer.lua
+++ /dev/null
@@ -1,1230 +0,0 @@
-local UTF8 = require((...):match("(.-)[^%.]+$") .. "UTF8")
-
----@class Renderer
----@field backgroundColor Color
----@field borderColor Color
----@field opacity number
----@field border {top:boolean, right:boolean, bottom:boolean, left:boolean}
----@field cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}
----@field theme string?
----@field themeComponent string?
----@field _themeState string
----@field imagePath string?
----@field image love.Image?
----@field _loadedImage love.Image?
----@field objectFit string
----@field objectPosition string
----@field imageOpacity number
----@field contentBlur {intensity:number, quality:number}?
----@field backdropBlur {intensity:number, quality:number}?
----@field _blurInstance table?
----@field _element Element?
----@field _Color Color
----@field _RoundedRect table
----@field _NinePatch table
----@field _ImageRenderer table
----@field _ImageCache table
----@field _Theme table
----@field _Transform Transform
----@field _Blur Blur
----@field _utils table
----@field _FONT_CACHE table
----@field _TextAlign table
----@field _ErrorHandler ErrorHandler
----@field _Performance Performance? Performance module dependency
-local Renderer = {}
-Renderer.__index = Renderer
-
---- Initialize module with shared dependencies
----@param deps table Dependencies {ErrorHandler, Performance}
-function Renderer.init(deps)
- Renderer._ErrorHandler = deps.ErrorHandler
- Renderer._Performance = deps.Performance
-end
-
---- Create a new Renderer instance
----@param config table Configuration table with rendering properties
----@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, Transform, utils}
-function Renderer.new(config, deps)
- local Color = deps.Color
- local ImageCache = deps.ImageCache
-
- local self = setmetatable({}, Renderer)
-
- -- Store dependencies for instance methods
- self._Color = Color
- self._RoundedRect = deps.RoundedRect
- self._NinePatch = deps.NinePatch
- self._ImageRenderer = deps.ImageRenderer
- self._ImageCache = ImageCache
- self._Theme = deps.Theme
- self._Blur = deps.Blur
- self._Transform = deps.Transform
- self._utils = deps.utils
- self._FONT_CACHE = deps.utils.FONT_CACHE
- self._TextAlign = deps.utils.enums.TextAlign
- self._TextAlignVertical = deps.utils.enums.TextAlignVertical
-
- -- Visual properties
- self.backgroundColor = config.backgroundColor or Color.new(0, 0, 0, 0)
- self.borderColor = config.borderColor or Color.new(0, 0, 0, 1)
- self.opacity = config.opacity or 1
-
- -- NOTE: border is intentionally NOT cached here. Renderer:draw resolves it from
- -- element.border (source of truth) so retained-mode bare writes and
- -- setProperty("border", ...) both take effect immediately.
-
- -- Corner radius
- self.cornerRadius = config.cornerRadius
- or {
- topLeft = 0,
- topRight = 0,
- bottomLeft = 0,
- bottomRight = 0,
- }
-
- -- Theme properties
- self.theme = config.theme
- self.themeComponent = config.themeComponent
- self._themeState = "normal"
-
- -- Image properties
- self.imagePath = config.imagePath
- self.image = config.image
- self._loadedImage = nil
- self.objectFit = config.objectFit or "fill"
- self.objectPosition = config.objectPosition or "center center"
- self.imageOpacity = config.imageOpacity or 1
- self.imageRepeat = config.imageRepeat or "no-repeat"
- self.imageTint = config.imageTint
-
- -- Blur effects
- self.contentBlur = config.contentBlur
- self.backdropBlur = config.backdropBlur
- self._blurInstance = nil
-
- -- Load image if path provided
- if self.imagePath and not self.image then
- local loadedImage = ImageCache.load(self.imagePath)
- if loadedImage then
- self._loadedImage = loadedImage
- else
- self._loadedImage = nil
- end
- elseif self.image then
- self._loadedImage = self.image
- else
- self._loadedImage = nil
- end
-
- return self
-end
-
---- Get or create blur instance for this element
----@return table|nil Blur instance or nil
-function Renderer:getBlurInstance()
- -- Determine quality from blur settings
- local quality = "medium"
- if self.contentBlur and self.contentBlur.quality then
- quality = self.contentBlur.quality
- elseif self.backdropBlur and self.backdropBlur.quality then
- quality = self.backdropBlur.quality
- end
-
- -- Map string quality to numeric quality (1-10)
- local numericQuality = 5 -- default medium
- if type(quality) == "string" then
- if quality == "low" then
- numericQuality = 3
- elseif quality == "medium" then
- numericQuality = 5
- elseif quality == "high" then
- numericQuality = 8
- end
- elseif type(quality) == "number" then
- numericQuality = quality
- end
-
- -- Create or reuse blur instance
- if not self._blurInstance or self._blurInstance.quality ~= numericQuality then
- self._blurInstance = self._Blur.new({ quality = numericQuality })
- end
-
- return self._blurInstance
-end
-
---- Set theme state (normal, hover, pressed, disabled, active)
----@param state string The theme state
-function Renderer:setThemeState(state)
- self._themeState = state
-end
-
---- Execute a single core draw command (background, image, theme, borders).
---- Commands are plain tables: { type = "background"|"image"|"theme"|"borders", ... }
----@param cmd table Command table
----@param ctx table Resolved draw context
-function Renderer:_executeDrawCommand(cmd, ctx)
- if cmd.type == "background" then
- local c = self._Color.new(cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a * ctx.opacity)
- love.graphics.setColor(c:toRGBA())
- self._RoundedRect.draw("fill", ctx.x, ctx.y, ctx.borderBoxWidth, ctx.borderBoxHeight, ctx.cornerRadius)
- elseif cmd.type == "image" then
- -- Image value props (imageOpacity/imageRepeat/imageTint/objectFit/
- -- objectPosition) and imagePath are read from the element as the single
- -- source of truth, so retained-mode bare writes (`element.imageOpacity = 0.5`),
- -- the setImage* setters, and setProperty(...) are all immediately
- -- consistent. The renderer's own config is a fallback for standalone
- -- Renderer usage with a sparse element (mirrors `element.onEvent or
- -- self.onEvent`); the integrated path always supplies an element whose
- -- _applyProps-bound values take precedence. _loadedImage intentionally
- -- remains on the renderer (the resolved love.Image from the Imageable load
- -- pipeline). See TestRetainedPropertyConsistency.
- local el = self._element
- local imageOpacity = (el and el.imageOpacity) or self.imageOpacity
- local imageRepeat = (el and el.imageRepeat) or self.imageRepeat
- local imageTint = (el and el.imageTint) or self.imageTint
- local objectFit = (el and el.objectFit) or self.objectFit
- local objectPosition = (el and el.objectPosition) or self.objectPosition
- local imagePath = (el and el.imagePath) or self.imagePath
- if not self._loadedImage then
- return
- end
- local img = self._loadedImage
- local imageX = ctx.x + ctx.paddingLeft
- local imageY = ctx.y + ctx.paddingTop
- local finalOpacity = ctx.opacity * imageOpacity
- local hasCornerRadius = false
- if ctx.cornerRadius then
- if type(ctx.cornerRadius) == "number" then
- hasCornerRadius = ctx.cornerRadius > 0
- else
- hasCornerRadius = ctx.cornerRadius.topLeft > 0
- or ctx.cornerRadius.topRight > 0
- or ctx.cornerRadius.bottomLeft > 0
- or ctx.cornerRadius.bottomRight > 0
- end
- end
- if hasCornerRadius then
- local success, err = pcall(function()
- love.graphics.stencil(function()
- self._RoundedRect.draw("fill", ctx.x, ctx.y, ctx.borderBoxWidth, ctx.borderBoxHeight, ctx.cornerRadius)
- end, "replace", 1)
- love.graphics.setStencilTest("greater", 0)
- end)
- if not success then
- if err and err:match("stencil") then
- local cr = ctx.cornerRadius
- local crStr = type(cr) == "number" and tostring(cr)
- or string.format("TL:%d TR:%d BL:%d BR:%d", cr.topLeft, cr.topRight, cr.bottomLeft, cr.bottomRight)
- Renderer._ErrorHandler:warn(
- "Renderer",
- "IMG_001",
- { imagePath = imagePath or "unknown", cornerRadius = crStr, error = tostring(err) }
- )
- hasCornerRadius = false
- else
- error(err, 2)
- end
- end
- end
- if imageRepeat and imageRepeat ~= "no-repeat" then
- self._ImageRenderer.drawTiled(
- img,
- imageX,
- imageY,
- ctx.contentWidth,
- ctx.contentHeight,
- imageRepeat,
- finalOpacity,
- imageTint
- )
- else
- self._ImageRenderer.draw(
- img,
- imageX,
- imageY,
- ctx.contentWidth,
- ctx.contentHeight,
- objectFit,
- objectPosition,
- finalOpacity,
- imageTint
- )
- end
- if hasCornerRadius then
- love.graphics.setStencilTest()
- end
- elseif cmd.type == "theme" then
- if not cmd.themeComponent then
- return
- end
- local themeToUse = nil
- if self.theme then
- themeToUse = self._Theme.get(self.theme)
- if not themeToUse then
- pcall(function()
- self._Theme.load(self.theme)
- end)
- themeToUse = self._Theme.get(self.theme)
- end
- else
- themeToUse = self._Theme.getActive()
- end
- if not themeToUse then
- return
- end
- local component = themeToUse.components[cmd.themeComponent]
- if not component then
- return
- end
- local state = self._themeState
- if state and component.states and component.states[state] then
- component = component.states[state]
- end
- local atlasToUse = component._loadedAtlas or themeToUse.atlas
- if atlasToUse and component.regions then
- local r = component.regions
- if
- r.topLeft
- and r.topCenter
- and r.topRight
- and r.middleLeft
- and r.middleCenter
- and r.middleRight
- and r.bottomLeft
- and r.bottomCenter
- and r.bottomRight
- then
- self._NinePatch.draw(
- component,
- atlasToUse,
- ctx.x,
- ctx.y,
- ctx.borderBoxWidth,
- ctx.borderBoxHeight,
- ctx.opacity,
- cmd.scaleCorners,
- cmd.scalingAlgorithm
- )
- end
- end
- elseif cmd.type == "borders" then
- local border = cmd.border
- if not border then
- return
- end
- local bc = cmd.borderColor
- local borderColorWithOpacity = self._Color.new(bc.r, bc.g, bc.b, bc.a * ctx.opacity)
- love.graphics.setColor(borderColorWithOpacity:toRGBA())
- local bw, bh = ctx.borderBoxWidth, ctx.borderBoxHeight
- if type(border) == "number" then
- love.graphics.setLineWidth(border)
- self._RoundedRect.draw("line", ctx.x, ctx.y, bw, bh, ctx.cornerRadius)
- love.graphics.setLineWidth(1)
- else
- local allBorders = border.top and border.bottom and border.left and border.right
- local uniformWidth = allBorders
- and type(border.top) == "number"
- and border.top == border.right
- and border.top == border.bottom
- and border.top == border.left
- if uniformWidth then
- love.graphics.setLineWidth(border.top)
- self._RoundedRect.draw("line", ctx.x, ctx.y, bw, bh, ctx.cornerRadius)
- love.graphics.setLineWidth(1)
- else
- if border.top then
- love.graphics.setLineWidth(type(border.top) == "number" and border.top or 1)
- love.graphics.line(ctx.x, ctx.y, ctx.x + bw, ctx.y)
- end
- if border.bottom then
- love.graphics.setLineWidth(type(border.bottom) == "number" and border.bottom or 1)
- love.graphics.line(ctx.x, ctx.y + bh, ctx.x + bw, ctx.y + bh)
- end
- if border.left then
- love.graphics.setLineWidth(type(border.left) == "number" and border.left or 1)
- love.graphics.line(ctx.x, ctx.y, ctx.x, ctx.y + bh)
- end
- if border.right then
- love.graphics.setLineWidth(type(border.right) == "number" and border.right or 1)
- love.graphics.line(ctx.x + bw, ctx.y, ctx.x + bw, ctx.y + bh)
- end
- love.graphics.setLineWidth(1)
- end
- end
- end
-end
-
---- Build the render command buffer: resolve draw properties once from the
---- element (source of truth) and return a flat command list + draw context.
----@param element table Element instance
----@param backdropCanvas table|nil
----@return table cmds, table ctx Command list and resolved context
-function Renderer:_buildCommands(element, backdropCanvas)
- local opacity = element.opacity ~= nil and element.opacity or 1
- local backgroundColor = element.backgroundColor or self._Color.new(0, 0, 0, 0)
- local borderColor = element.borderColor or self._Color.new(0, 0, 0, 1)
- local cornerRadius = element.cornerRadius ~= nil and element.cornerRadius or nil
- local themeComponent = element.themeComponent
- local border = element.border
- local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- local borderBoxHeight = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
-
- -- Handle opacity during animation
- local drawBackgroundColor = backgroundColor
- if element.animation then
- local anim = element.animation:interpolate()
- if anim.opacity then
- drawBackgroundColor = self._Color.new(backgroundColor.r, backgroundColor.g, backgroundColor.b, anim.opacity)
- end
- end
-
- -- Build resolved context (shared by all commands — eliminates per-method params)
- local ctx = {
- x = element.x,
- y = element.y,
- opacity = opacity,
- cornerRadius = cornerRadius,
- borderBoxWidth = borderBoxWidth,
- borderBoxHeight = borderBoxHeight,
- paddingLeft = element.padding.left,
- paddingTop = element.padding.top,
- contentWidth = element.width,
- contentHeight = element.height,
- backdropCanvas = backdropCanvas,
- }
-
- -- Build command list (conditional — only emit layers that have data)
- local cmds = {}
- local n = 0
-
- -- LAYER 0.5: backdrop blur (handled separately, not a command — needs canvas access)
- if self.backdropBlur and self.backdropBlur.radius > 0 then
- n = n + 1
- cmds[n] = { type = "backdropBlur", radius = self.backdropBlur.radius } -- executed before background
- end
-
- -- LAYER 1: background
- n = n + 1
- cmds[n] = { type = "background", color = drawBackgroundColor }
-
- -- LAYER 1.5: image (always emit; _executeCommand early-exits if no image)
- n = n + 1
- cmds[n] = { type = "image" }
-
- -- LAYER 2: theme 9-patch
- n = n + 1
- cmds[n] = {
- type = "theme",
- themeComponent = themeComponent,
- scaleCorners = element.scaleCorners,
- scalingAlgorithm = element.scalingAlgorithm,
- }
-
- -- LAYER 3: borders
- n = n + 1
- cmds[n] = { type = "borders", borderColor = borderColor, border = border }
-
- -- LAYER 4: text (cursor, selection, placeholder, password masking)
- n = n + 1
- cmds[n] = { type = "text" }
-
- -- LAYER 4.5: custom draw callback (if provided)
- if element.customDraw then
- n = n + 1
- cmds[n] = { type = "customDraw" }
- end
-
- -- NOTE: pressed-state overlay (former Layer 5) is now owned by the Clickable
- -- behavior's onDraw, dispatched from Element:draw. The renderer no longer
- -- branches on element.onEvent for press feedback.
-
- return cmds, ctx
-end
-
---- Execute a special render command (backdropBlur, customDraw, pressedState).
---- These interact with love.graphics state in non-uniform ways and are handled separately
---- from the core draw commands (background/image/theme/borders).
----@param cmd table Command table
----@param ctx table Resolved draw context
-function Renderer:_executeSpecialCommand(cmd, ctx)
- if cmd.type == "backdropBlur" then
- if ctx.backdropCanvas then
- local blurInstance = self:getBlurInstance()
- if blurInstance then
- local eid = self._element and self._element.id and self._element.id ~= "" and self._element.id or nil
- blurInstance:applyBackdropCached(
- cmd.radius,
- ctx.x,
- ctx.y,
- ctx.borderBoxWidth,
- ctx.borderBoxHeight,
- ctx.backdropCanvas,
- eid
- )
- end
- end
- elseif cmd.type == "text" then
- self:drawText(self._element)
- elseif cmd.type == "customDraw" then
- love.graphics.push()
- love.graphics.setColor(1, 1, 1, 1)
- self._element.customDraw(self._element)
- love.graphics.pop()
- end
-end
-
---- Main draw method - renders all visual layers via command buffer.
----@param element Element The parent Element instance
----@param backdropCanvas table|nil Backdrop canvas for backdrop blur
-function Renderer:draw(element, backdropCanvas)
- self._element = element -- cache for customDraw/pressedState
-
- if not element then
- Renderer._ErrorHandler:warn("Renderer", "SYS_002", { method = "draw" })
- return
- end
-
- -- Start performance timing
- local elementId
- if Renderer._Performance and Renderer._Performance.enabled then
- elementId = element.id or "unnamed"
- Renderer._Performance:startTimer("render_" .. elementId)
- Renderer._Performance:incrementCounter("draw_calls", 1)
- end
-
- -- Early exit if element is invisible (optimization)
- if element.opacity ~= nil and element.opacity <= 0 then
- if Renderer._Performance and Renderer._Performance.enabled and elementId then
- Renderer._Performance:stopTimer("render_" .. elementId)
- end
- return
- end
-
- -- Build command buffer + resolve draw context once
- local cmds, ctx = self:_buildCommands(element, backdropCanvas)
-
- -- Apply transform if exists
- local hasTransform = element.transform and self._Transform and not self._Transform.isIdentity(element.transform)
- if hasTransform then
- self._Transform.apply(element.transform, element.x, element.y, element.width, element.height)
- end
-
- -- Execute all commands in order
- for _, cmd in ipairs(cmds) do
- -- Draw commands (background, image, theme, borders) use the core executor;
- -- special commands (backdropBlur, customDraw, pressedState) are handled in _executeCommand.
- local typ = cmd.type
- if typ == "background" or typ == "image" or typ == "theme" or typ == "borders" then
- self:_executeDrawCommand(cmd, ctx)
- else
- self:_executeSpecialCommand(cmd, ctx)
- end
- end
-
- -- Unapply transform if it was applied
- if hasTransform then
- self._Transform.unapply()
- end
-
- -- Stop performance timing
- if Renderer._Performance and Renderer._Performance.enabled and elementId then
- Renderer._Performance:stopTimer("render_" .. elementId)
- end
-end
-
---- Get font for element (resolves from theme or fontFamily)
----@param element table Reference to the parent Element instance
----@return love.Font
-function Renderer:getFont(element)
- return self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager)
-end
-
---- Wrap a line of text based on element's textWrap mode
----@param element table Reference to the parent Element instance
----@param line string The line of text to wrap
----@param maxWidth number Maximum width for wrapping
----@return table Array of {text, startIdx, endIdx}
-function Renderer:wrapLine(element, line, maxWidth)
- -- UTF-8 support
- local utf8 = UTF8
-
- if not element.editable then
- return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
- end
-
- local font = self:getFont(element)
- local wrappedParts = {}
- local currentLine = ""
- local startIdx = 0
-
- -- Helper function to extract a UTF-8 character by character index
- local function getUtf8Char(str, charIndex)
- local byteStart = utf8.offset(str, charIndex)
- if not byteStart then
- return ""
- end
- local byteEnd = utf8.offset(str, charIndex + 1)
- if byteEnd then
- return str:sub(byteStart, byteEnd - 1)
- else
- return str:sub(byteStart)
- end
- end
-
- if element.textWrap == "word" then
- -- Tokenize into words and whitespace, preserving exact spacing
- local tokens = {}
- local pos = 1
- local lineLen = utf8.len(line)
-
- while pos <= lineLen do
- -- Check if current position is whitespace
- local char = getUtf8Char(line, pos)
- if char:match("%s") then
- -- Collect whitespace sequence
- local wsStart = pos
- while pos <= lineLen and getUtf8Char(line, pos):match("%s") do
- pos = pos + 1
- end
- table.insert(tokens, {
- type = "space",
- text = line:sub(utf8.offset(line, wsStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
- startPos = wsStart - 1,
- length = pos - wsStart,
- })
- else
- -- Collect word (non-whitespace sequence)
- local wordStart = pos
- while pos <= lineLen and not getUtf8Char(line, pos):match("%s") do
- pos = pos + 1
- end
- table.insert(tokens, {
- type = "word",
- text = line:sub(utf8.offset(line, wordStart), utf8.offset(line, pos) and utf8.offset(line, pos) - 1 or #line),
- startPos = wordStart - 1,
- length = pos - wordStart,
- })
- end
- end
-
- -- Process tokens and wrap
- local charPos = 0 -- Track our position in the original line
- for _, token in ipairs(tokens) do
- if token.type == "word" then
- local testLine = currentLine .. token.text
- local width = font:getWidth(testLine)
-
- if width > maxWidth and currentLine ~= "" then
- -- Current line is full, wrap before this word
- local currentLineLen = utf8.len(currentLine)
- table.insert(wrappedParts, {
- text = currentLine,
- startIdx = startIdx,
- endIdx = startIdx + currentLineLen,
- })
- startIdx = charPos
- currentLine = token.text
- charPos = charPos + token.length
-
- -- Check if the word itself is too long - if so, break it with character wrapping
- if font:getWidth(token.text) > maxWidth then
- local wordLen = utf8.len(token.text)
- local charLine = ""
- local charStartIdx = startIdx
-
- for j = 1, wordLen do
- local char = getUtf8Char(token.text, j)
- local testCharLine = charLine .. char
- local charWidth = font:getWidth(testCharLine)
-
- if charWidth > maxWidth and charLine ~= "" then
- table.insert(wrappedParts, {
- text = charLine,
- startIdx = charStartIdx,
- endIdx = charStartIdx + utf8.len(charLine),
- })
- charStartIdx = charStartIdx + utf8.len(charLine)
- charLine = char
- else
- charLine = testCharLine
- end
- end
-
- currentLine = charLine
- startIdx = charStartIdx
- end
- elseif width > maxWidth and currentLine == "" then
- -- Word is too long to fit on a line by itself - use character wrapping
- local wordLen = utf8.len(token.text)
- local charLine = ""
- local charStartIdx = startIdx
-
- for j = 1, wordLen do
- local char = getUtf8Char(token.text, j)
- local testCharLine = charLine .. char
- local charWidth = font:getWidth(testCharLine)
-
- if charWidth > maxWidth and charLine ~= "" then
- table.insert(wrappedParts, {
- text = charLine,
- startIdx = charStartIdx,
- endIdx = charStartIdx + utf8.len(charLine),
- })
- charStartIdx = charStartIdx + utf8.len(charLine)
- charLine = char
- else
- charLine = testCharLine
- end
- end
-
- currentLine = charLine
- startIdx = charStartIdx
- charPos = charPos + token.length
- else
- currentLine = testLine
- charPos = charPos + token.length
- end
- else
- -- It's whitespace - add to current line
- currentLine = currentLine .. token.text
- charPos = charPos + token.length
- end
- end
- else
- -- Character wrapping
- local lineLength = utf8.len(line)
- for i = 1, lineLength do
- local char = getUtf8Char(line, i)
- local testLine = currentLine .. char
- local width = font:getWidth(testLine)
-
- if width > maxWidth and currentLine ~= "" then
- table.insert(wrappedParts, {
- text = currentLine,
- startIdx = startIdx,
- endIdx = startIdx + utf8.len(currentLine),
- })
- currentLine = char
- startIdx = i - 1
- else
- currentLine = testLine
- end
- end
- end
-
- -- Add remaining text
- if currentLine ~= "" then
- table.insert(wrappedParts, {
- text = currentLine,
- startIdx = startIdx,
- endIdx = startIdx + utf8.len(currentLine),
- })
- end
-
- -- Ensure at least one part
- if #wrappedParts == 0 then
- table.insert(wrappedParts, {
- text = "",
- startIdx = 0,
- endIdx = 0,
- })
- end
-
- return wrappedParts
-end
-
---- Draw text content (includes text, cursor, selection, placeholder, password masking)
----@param element table Reference to the parent Element instance
-function Renderer:drawText(element)
- -- Update text layout if dirty (for multiline auto-grow)
- if element._textEditor then
- element._textEditor:_updateTextIfDirty(element)
- element._textEditor:updateAutoGrowHeight(element)
- end
-
- -- For editable elements, use TextEditor buffer; for non-editable, use text
- local displayText = element._textEditor and element._textEditor:getText() or element.text
- local isPlaceholder = false
-
- -- Show placeholder if editable and empty
- if element.editable and (not displayText or displayText == "") and element.placeholder then
- displayText = element.placeholder
- isPlaceholder = true
- end
-
- -- Apply password masking if enabled
- if element.passwordMode and displayText and displayText ~= "" and not isPlaceholder then
- local maskedText = string.rep("•", UTF8.len(displayText))
- displayText = maskedText
- end
-
- if displayText and displayText ~= "" then
- local textColor = isPlaceholder
- and self._Color.new(
- element.textColor.r * 0.5,
- element.textColor.g * 0.5,
- element.textColor.b * 0.5,
- element.textColor.a * 0.5
- )
- or element.textColor
- local textColorOpacity = element.opacity ~= nil and element.opacity or (self.opacity or 1)
- local textColorWithOpacity = self._Color.new(textColor.r, textColor.g, textColor.b, textColor.a * textColorOpacity)
- love.graphics.setColor(textColorWithOpacity:toRGBA())
-
- local origFont = love.graphics.getFont()
- if element.textSize then
- -- Use cached font instead of creating new one every frame
- local font =
- self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager)
- love.graphics.setFont(font)
- end
- local font = love.graphics.getFont()
- local textWidth = font:getWidth(displayText)
- local textHeight = font:getHeight()
- local tx, ty
-
- -- Text is drawn in the content box (inside padding)
- -- For 9-patch components, use contentPadding if available
- local textPaddingLeft = element.padding.left
- local textPaddingTop = element.padding.top
- local textAreaWidth = element.width
- local textAreaHeight = element.height
-
- -- Check if we should use 9-patch contentPadding for text positioning
- local scaledContentPadding = element:getScaledContentPadding()
- if scaledContentPadding then
- local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- local borderBoxHeight = element._borderBoxHeight
- or (element.height + element.padding.top + element.padding.bottom)
-
- textPaddingLeft = scaledContentPadding.left
- textPaddingTop = scaledContentPadding.top
- textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
- textAreaHeight = borderBoxHeight - scaledContentPadding.top - scaledContentPadding.bottom
- end
-
- local contentX = element.x + textPaddingLeft
- local contentY = element.y + textPaddingTop
-
- -- Resolve horizontal and vertical alignment (new format with backward compatibility)
- local hAlign = element.textAlignHorizontal or element.textAlign or self._TextAlign.START
- local vAlign = element.textAlignVertical or self._TextAlignVertical.START
-
- -- Check if text wrapping is enabled
- if element.textWrap and (element.textWrap == "word" or element.textWrap == "char" or element.textWrap == true) then
- -- Use printf for wrapped text (horizontal alignment only)
- local align = "left"
- if hAlign == self._TextAlign.CENTER then
- align = "center"
- elseif hAlign == self._TextAlign.END then
- align = "right"
- elseif hAlign == self._TextAlign.JUSTIFY then
- align = "justify"
- end
-
- tx = contentX
- ty = contentY
-
- -- Use printf with the available width for wrapping
- love.graphics.printf(displayText, tx, ty, textAreaWidth, align)
- else
- -- Use regular print for non-wrapped text
- -- Horizontal alignment
- if hAlign == self._TextAlign.START then
- tx = contentX
- elseif hAlign == self._TextAlign.CENTER then
- tx = contentX + (textAreaWidth - textWidth) / 2
- elseif hAlign == self._TextAlign.END then
- tx = contentX + textAreaWidth - textWidth - 10
- else -- JUSTIFY or unknown
- tx = contentX
- end
-
- -- Vertical alignment
- if vAlign == self._TextAlignVertical.START then
- ty = contentY
- elseif vAlign == self._TextAlignVertical.CENTER then
- ty = contentY + (textAreaHeight - textHeight) / 2
- elseif vAlign == self._TextAlignVertical.END then
- ty = contentY + textAreaHeight - textHeight
- else
- ty = contentY
- end
-
- -- Apply scroll offset for editable single-line inputs
- if element.editable and not element.multiline and element._textScrollX then
- tx = tx - element._textScrollX
- end
-
- -- Use scissor to clip text to content area for editable inputs
- if element.editable and not element.multiline then
- love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight)
- end
-
- love.graphics.print(displayText, tx, ty)
-
- -- Reset scissor
- if element.editable and not element.multiline then
- love.graphics.setScissor()
- end
- end
-
- -- Draw cursor for focused editable elements (even if text is empty)
- if element._textEditor and element._textEditor:isFocused() and element._textEditor._cursorVisible then
- local cursorColor = element.cursorColor or element.textColor
- local elemOpacity = element.opacity ~= nil and element.opacity or (self.opacity or 1)
- local cursorWithOpacity =
- self._Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * elemOpacity)
- love.graphics.setColor(cursorWithOpacity:toRGBA())
-
- -- Calculate cursor position using TextEditor method
- local cursorRelX, cursorRelY = element._textEditor:_getCursorScreenPosition(element)
- local cursorX = contentX + cursorRelX
- local cursorY = contentY + cursorRelY
- local cursorHeight = textHeight
-
- -- Apply scroll offset for single-line inputs
- if not element.multiline and element._textEditor._textScrollX then
- cursorX = cursorX - element._textEditor._textScrollX
- end
-
- -- Apply scissor for single-line editable inputs
- if not element.multiline then
- love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight)
- end
-
- -- Draw cursor line
- love.graphics.rectangle("fill", cursorX, cursorY, 2, cursorHeight)
-
- -- Reset scissor
- if not element.multiline then
- love.graphics.setScissor()
- end
- end
-
- -- Draw selection highlight for editable elements
- if element._textEditor and element._textEditor:isFocused() and element._textEditor:hasSelection() then
- -- For editable elements, check TextEditor buffer instead of element.text
- local textBuffer = element._textEditor:getText()
- if textBuffer and textBuffer ~= "" then
- local selStart, selEnd = element._textEditor:getSelection()
- local selectionColor = element.selectionColor or self._Color.new(0.3, 0.5, 0.8, 0.5)
- local elemOpacity = element.opacity ~= nil and element.opacity or (self.opacity or 1)
- local selectionWithOpacity =
- self._Color.new(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * elemOpacity)
-
- -- Get selection rectangles from TextEditor
- local selectionRects = element._textEditor:_getSelectionRects(element, selStart, selEnd)
-
- -- Apply scissor for single-line editable inputs
- if not element.multiline then
- love.graphics.setScissor(contentX, contentY, textAreaWidth, textAreaHeight)
- end
-
- -- Draw selection background rectangles
- love.graphics.setColor(selectionWithOpacity:toRGBA())
- for _, rect in ipairs(selectionRects) do
- local rectX = contentX + rect.x
- local rectY = contentY + rect.y
- if not element.multiline and element._textEditor._textScrollX then
- rectX = rectX - element._textEditor._textScrollX
- end
- love.graphics.rectangle("fill", rectX, rectY, rect.width, rect.height)
- end
-
- -- Reset scissor
- if not element.multiline then
- love.graphics.setScissor()
- end
- end
- end
-
- if element.textSize then
- love.graphics.setFont(origFont)
- end
- end
-
- -- Draw cursor for focused editable elements even when empty
- if
- element._textEditor
- and element._textEditor:isFocused()
- and element._textEditor._cursorVisible
- and (not displayText or displayText == "")
- then
- -- Set up font for cursor rendering
- local origFont = love.graphics.getFont()
- if element.textSize then
- local font =
- self._utils.getFont(element.textSize, element.fontFamily, element.themeComponent, element._themeManager)
- love.graphics.setFont(font)
- end
-
- local font = love.graphics.getFont()
- local textHeight = font:getHeight()
-
- -- Calculate text area position
- local textPaddingLeft = element.padding.left
- local textPaddingTop = element.padding.top
- local scaledContentPadding = element:getScaledContentPadding()
- if scaledContentPadding then
- textPaddingLeft = scaledContentPadding.left
- textPaddingTop = scaledContentPadding.top
- end
-
- local contentX = element.x + textPaddingLeft
- local contentY = element.y + textPaddingTop
-
- -- Draw cursor
- local cursorColor = element.cursorColor or element.textColor
- local elemOpacity = element.opacity ~= nil and element.opacity or (self.opacity or 1)
- local cursorWithOpacity = self._Color.new(cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a * elemOpacity)
- love.graphics.setColor(cursorWithOpacity:toRGBA())
- love.graphics.rectangle("fill", contentX, contentY, 2, textHeight)
-
- if element.textSize then
- love.graphics.setFont(origFont)
- end
- end
-end
-
---- Draw scrollbars (both vertical and horizontal)
----@param element table Reference to the parent Element instance
----@param x number X position
----@param y number Y position
----@param w number Width
----@param h number Height
----@param dims table Scrollbar dimensions from _calculateScrollbarDimensions
-function Renderer:drawScrollbars(element, x, y, w, h, dims)
- -- Try to get themed scrollbar component
- local scrollbarComponent = nil
- if element.scrollBarStyle or self._Theme.hasActive() then
- scrollbarComponent = self._Theme.getScrollbar(element.scrollBarStyle)
- end
-
- -- Vertical scrollbar
- if dims.vertical.visible and not element.hideScrollbars.vertical then
- -- Position scrollbar within content area (x, y is border-box origin)
- local contentX = x + element.padding.left
- local contentY = y + element.padding.top
- local trackX = contentX + w - element.scrollbarWidth - element.scrollbarPadding
- local trackY = contentY + element.scrollbarPadding
-
- -- Check if we should use themed rendering
- if scrollbarComponent then
- -- Themed scrollbar rendering using NinePatch
- local frameComponent = scrollbarComponent.frame or scrollbarComponent
- local barComponent = scrollbarComponent.bar or scrollbarComponent
-
- -- Calculate knob offset (element overrides theme)
- local knobOffsetX = 0
- local knobOffsetY = 0
-
- -- Use element offset if provided, otherwise use theme offset
- if element.scrollbarKnobOffset then
- knobOffsetX = element.scrollbarKnobOffset.x or 0
- knobOffsetY = element.scrollbarKnobOffset.vertical or 0
- elseif barComponent and barComponent.knobOffset then
- local themeOffset = self._utils.normalizeOffsetTable(barComponent.knobOffset, 0)
- knobOffsetX = themeOffset.x
- knobOffsetY = themeOffset.vertical
- end
-
- -- Extract contentPadding top inset from frame for knob sizing.
- -- Vertical scrollbar only consumes framePaddingTop; other insets are unused.
- local framePaddingTop = 0
- if frameComponent and frameComponent._ninePatchData and frameComponent._ninePatchData.contentPadding then
- framePaddingTop = frameComponent._ninePatchData.contentPadding.top or 0
- end
-
- -- Draw track (frame) if component exists
- if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
- self._NinePatch.draw(
- frameComponent,
- frameComponent._loadedAtlas,
- trackX,
- trackY,
- element.scrollbarWidth,
- dims.vertical.trackHeight
- )
- end
-
- -- Draw thumb (bar) if component exists
- if barComponent and barComponent._loadedAtlas and barComponent.regions then
- -- Adjust knob dimensions to account for frame's contentPadding
- -- Vertical scrollbar: width affected by left+right, height affected by top+bottom
- local knobWidth = element.scrollbarWidth
- local knobHeight = dims.vertical.thumbHeight - framePaddingTop / 2
- self._NinePatch.draw(
- barComponent,
- barComponent._loadedAtlas,
- trackX + knobOffsetX,
- trackY + dims.vertical.thumbY + knobOffsetY,
- knobWidth,
- knobHeight
- )
- end
- else
- -- Fallback to color-based rendering
- -- Determine thumb color based on state (independent for vertical)
- local thumbColor = element.scrollbarColor
- if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then
- -- Active state: brighter
- local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
- thumbColor = self._Color.new(r, g, b, a)
- elseif element._scrollbarHoveredVertical then
- -- Hover state: slightly brighter
- local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
- thumbColor = self._Color.new(r, g, b, a)
- end
-
- -- Draw track
- love.graphics.setColor(element.scrollbarTrackColor:toRGBA())
- love.graphics.rectangle(
- "fill",
- trackX,
- trackY,
- element.scrollbarWidth,
- dims.vertical.trackHeight,
- element.scrollbarRadius
- )
-
- -- Draw thumb with state-based color
- love.graphics.setColor(thumbColor:toRGBA())
- love.graphics.rectangle(
- "fill",
- trackX,
- trackY + dims.vertical.thumbY,
- element.scrollbarWidth,
- dims.vertical.thumbHeight,
- element.scrollbarRadius
- )
- end
- end
-
- -- Horizontal scrollbar
- if dims.horizontal.visible and not element.hideScrollbars.horizontal then
- -- Position scrollbar within content area (x, y is border-box origin)
- local contentX = x + element.padding.left
- local contentY = y + element.padding.top
- local trackX = contentX + element.scrollbarPadding
- local trackY = contentY + h - element.scrollbarWidth - element.scrollbarPadding
-
- -- Check if we should use themed rendering
- if scrollbarComponent then
- -- Themed scrollbar rendering using NinePatch
- local frameComponent = scrollbarComponent.frame or scrollbarComponent
- local barComponent = scrollbarComponent.bar or scrollbarComponent
-
- -- Calculate knob offset (element overrides theme)
- local knobOffsetX = 0
- local knobOffsetY = 0
-
- -- Use element offset if provided, otherwise use theme offset
- if element.scrollbarKnobOffset then
- knobOffsetX = element.scrollbarKnobOffset.horizontal or 0
- knobOffsetY = element.scrollbarKnobOffset.y or 0
- elseif barComponent and barComponent.knobOffset then
- local themeOffset = self._utils.normalizeOffsetTable(barComponent.knobOffset, 0)
- knobOffsetX = themeOffset.horizontal
- knobOffsetY = themeOffset.y
- end
-
- -- Extract contentPadding from frame for knob sizing (horizontal: right inset unused).
- local framePaddingLeft = 0
- local framePaddingTop = 0
- local framePaddingBottom = 0
- if frameComponent and frameComponent._ninePatchData and frameComponent._ninePatchData.contentPadding then
- framePaddingLeft = frameComponent._ninePatchData.contentPadding.left or 0
- framePaddingTop = frameComponent._ninePatchData.contentPadding.top or 0
- framePaddingBottom = frameComponent._ninePatchData.contentPadding.bottom or 0
- end
-
- -- Draw track (frame) if component exists
- if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
- self._NinePatch.draw(
- frameComponent,
- frameComponent._loadedAtlas,
- trackX,
- trackY,
- dims.horizontal.trackWidth,
- element.scrollbarWidth
- )
- end
-
- -- Draw thumb (bar) if component exists
- if barComponent and barComponent._loadedAtlas and barComponent.regions then
- -- Adjust knob dimensions to account for frame's contentPadding
- -- Horizontal scrollbar: width affected by left+right, height affected by top+bottom
- local knobWidth = dims.horizontal.thumbWidth - framePaddingLeft / 2
- local knobHeight = element.scrollbarWidth - framePaddingTop - framePaddingBottom
- self._NinePatch.draw(
- barComponent,
- barComponent._loadedAtlas,
- trackX + dims.horizontal.thumbX + knobOffsetX,
- trackY + knobOffsetY,
- knobWidth,
- knobHeight
- )
- end
- else
- -- Fallback to color-based rendering
- -- Determine thumb color based on state (independent for horizontal)
- local thumbColor = element.scrollbarColor
- if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then
- -- Active state: brighter
- local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
- thumbColor = self._Color.new(r, g, b, a)
- elseif element._scrollbarHoveredHorizontal then
- -- Hover state: slightly brighter
- local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
- thumbColor = self._Color.new(r, g, b, a)
- end
-
- -- Draw track
- love.graphics.setColor(element.scrollbarTrackColor:toRGBA())
- love.graphics.rectangle(
- "fill",
- trackX,
- trackY,
- dims.horizontal.trackWidth,
- element.scrollbarWidth,
- element.scrollbarRadius
- )
-
- -- Draw thumb with state-based color
- love.graphics.setColor(thumbColor:toRGBA())
- love.graphics.rectangle(
- "fill",
- trackX + dims.horizontal.thumbX,
- trackY,
- dims.horizontal.thumbWidth,
- element.scrollbarWidth,
- element.scrollbarRadius
- )
- end
- end
-
- -- Reset color
- love.graphics.setColor(1, 1, 1, 1)
-end
-
---- Draw visual feedback when element is pressed
----@param x number X position
----@param y number Y position
----@param borderBoxWidth number Border box width
----@param borderBoxHeight number Border box height
----@param opacity number Element opacity
----@param cornerRadius number|table Corner radius
-function Renderer:drawPressedState(x, y, borderBoxWidth, borderBoxHeight, opacity, cornerRadius)
- love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * (opacity or 1))
- self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, cornerRadius)
-end
-
---- Cleanup renderer resources
-function Renderer:destroy()
- self._loadedImage = nil
- self._blurInstance = nil
-end
-
-return Renderer
diff --git a/libs/flexlove/modules/RoundedRect.lua b/libs/flexlove/modules/RoundedRect.lua
deleted file mode 100644
index 8db7222d..00000000
--- a/libs/flexlove/modules/RoundedRect.lua
+++ /dev/null
@@ -1,124 +0,0 @@
-local RoundedRect = {}
-
---- Generate points for a rounded rectangle
----@param x number
----@param y number
----@param width number
----@param height number
----@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}|number
----@param segments number? -- Number of segments per corner arc (default: 10)
----@return table -- Array of vertices for love.graphics.polygon
-function RoundedRect.getPoints(x, y, width, height, cornerRadius, segments)
- segments = segments or 10
- local points = {}
-
- -- Helper to add arc points
- local function addArc(cx, cy, radius, startAngle, endAngle)
- if radius <= 0 then
- table.insert(points, cx)
- table.insert(points, cy)
- return
- end
-
- for i = 0, segments do
- local angle = startAngle + (endAngle - startAngle) * (i / segments)
- table.insert(points, cx + math.cos(angle) * radius)
- table.insert(points, cy + math.sin(angle) * radius)
- end
- end
-
- -- Handle uniform corner radius (number)
- if type(cornerRadius) == "number" then
- cornerRadius = {
- topLeft = cornerRadius,
- topRight = cornerRadius,
- bottomLeft = cornerRadius,
- bottomRight = cornerRadius,
- }
- end
-
- local r1 = math.min(cornerRadius.topLeft, width / 2, height / 2)
- local r2 = math.min(cornerRadius.topRight, width / 2, height / 2)
- local r3 = math.min(cornerRadius.bottomRight, width / 2, height / 2)
- local r4 = math.min(cornerRadius.bottomLeft, width / 2, height / 2)
-
- -- Top-right corner
- addArc(x + width - r2, y + r2, r2, -math.pi / 2, 0)
-
- -- Bottom-right corner
- addArc(x + width - r3, y + height - r3, r3, 0, math.pi / 2)
-
- -- Bottom-left corner
- addArc(x + r4, y + height - r4, r4, math.pi / 2, math.pi)
-
- -- Top-left corner
- addArc(x + r1, y + r1, r1, math.pi, math.pi * 1.5)
-
- return points
-end
-
---- Draw a filled rounded rectangle
----@param mode string -- "fill" or "line"
----@param x number
----@param y number
----@param width number
----@param height number
----@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}|number|nil
-function RoundedRect.draw(mode, x, y, width, height, cornerRadius)
- -- OPTIMIZATION: Handle nil cornerRadius (no rounding)
- if not cornerRadius then
- love.graphics.rectangle(mode, x, y, width, height)
- return
- end
-
- -- Handle uniform corner radius (number)
- if type(cornerRadius) == "number" then
- if cornerRadius <= 0 then
- love.graphics.rectangle(mode, x, y, width, height)
- return
- end
- -- Convert to table format for processing
- cornerRadius = {
- topLeft = cornerRadius,
- topRight = cornerRadius,
- bottomLeft = cornerRadius,
- bottomRight = cornerRadius,
- }
- end
-
- -- Check if any corners are rounded
- local hasRoundedCorners = cornerRadius.topLeft > 0
- or cornerRadius.topRight > 0
- or cornerRadius.bottomLeft > 0
- or cornerRadius.bottomRight > 0
-
- if not hasRoundedCorners then
- -- No rounded corners, use regular rectangle
- love.graphics.rectangle(mode, x, y, width, height)
- return
- end
-
- local points = RoundedRect.getPoints(x, y, width, height, cornerRadius)
-
- if mode == "fill" then
- love.graphics.polygon("fill", points)
- else
- -- For line mode, draw the outline
- love.graphics.polygon("line", points)
- end
-end
-
---- Create a stencil function for rounded rectangle clipping
----@param x number
----@param y number
----@param width number
----@param height number
----@param cornerRadius {topLeft:number, topRight:number, bottomLeft:number, bottomRight:number}|number|nil
----@return function
-function RoundedRect.stencilFunction(x, y, width, height, cornerRadius)
- return function()
- RoundedRect.draw("fill", x, y, width, height, cornerRadius)
- end
-end
-
-return RoundedRect
diff --git a/libs/flexlove/modules/ScrollManager.lua b/libs/flexlove/modules/ScrollManager.lua
deleted file mode 100644
index a23d4d19..00000000
--- a/libs/flexlove/modules/ScrollManager.lua
+++ /dev/null
@@ -1,1479 +0,0 @@
----@class ScrollManager
----@field overflow string -- "visible"|"hidden"|"auto"|"scroll"
----@field overflowX string? -- X-axis specific overflow (overrides overflow)
----@field overflowY string? -- Y-axis specific overflow (overrides overflow)
----@field scrollbarWidth number -- Width/height of scrollbar track
----@field scrollbarColor Color -- Scrollbar thumb color
----@field scrollbarTrackColor Color -- Scrollbar track background color
----@field scrollbarRadius number -- Border radius for scrollbars
----@field scrollbarPadding number -- Padding around scrollbar
----@field scrollSpeed number -- Scroll speed for wheel events (pixels per wheel unit)
----@field invertScroll boolean -- Invert mouse wheel scroll direction (default: false)
----@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
----@field scrollbarKnobOffset table -- {x: number, y: number, horizontal: number, vertical: number} -- Offset for scrollbar knob/handle position
----@field hideScrollbars table -- {vertical: boolean, horizontal: boolean}
----@field scrollbarPlacement string -- "reserve-space"|"overlay" -- Whether scrollbar reserves space or overlays content (default: "reserve-space")
----@field scrollbarBalance boolean -- When true, reserve space on both sides of content for visual balance (default: false)
----@field touchScrollEnabled boolean -- Enable touch scrolling
----@field momentumScrollEnabled boolean -- Enable momentum scrolling
----@field bounceEnabled boolean -- Enable bounce effects at boundaries
----@field scrollFriction number -- Friction coefficient for momentum (0.95-0.98)
----@field bounceStiffness number -- Bounce spring constant (0.1-0.3)
----@field maxOverscroll number -- Maximum overscroll distance (pixels)
----@field _overflowX boolean -- True if content overflows horizontally
----@field _overflowY boolean -- True if content overflows vertically
----@field _contentWidth number -- Total content width (including overflow)
----@field _contentHeight number -- Total content height (including overflow)
----@field _scrollX number -- Current horizontal scroll position
----@field _scrollY number -- Current vertical scroll position
----@field _targetScrollX number? -- Target scroll X for smooth scrolling
----@field _targetScrollY number? -- Target scroll Y for smooth scrolling
----@field _smoothScrollSpeed number -- Speed of smooth scroll interpolation (0-1, higher = faster)
----@field _maxScrollX number -- Maximum horizontal scroll (contentWidth - containerWidth)
----@field _maxScrollY number -- Maximum vertical scroll (contentHeight - containerHeight)
----@field _scrollbarHoveredVertical boolean -- True if mouse is over vertical scrollbar
----@field _scrollbarHoveredHorizontal boolean -- True if mouse is over horizontal scrollbar
----@field _scrollbarDragging boolean -- True if currently dragging a scrollbar
----@field _hoveredScrollbar string? -- "vertical" or "horizontal" when dragging
----@field _scrollbarDragOffset number -- DEPRECATED: Offset from thumb top when drag started (kept for compatibility)
----@field _dragStartMouseX number -- Mouse X position when drag started
----@field _dragStartMouseY number -- Mouse Y position when drag started
----@field _dragStartScrollX number -- Scroll X position when drag started
----@field _dragStartScrollY number -- Scroll Y position when drag started
----@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame
----@field _touchScrolling boolean -- True if currently touch scrolling
----@field _scrollVelocityX number -- Current horizontal scroll velocity (px/s)
----@field _scrollVelocityY number -- Current vertical scroll velocity (px/s)
----@field _momentumScrolling boolean -- True if momentum scrolling is active
----@field _lastTouchTime number -- Timestamp of last touch move
----@field _lastTouchX number -- Last touch X position
----@field _lastTouchY number -- Last touch Y position
----@field _Color table
----@field _utils table
----@field _ErrorHandler table? ErrorHandler module dependency
-local ScrollManager = {}
-ScrollManager.__index = ScrollManager
-
---- Initialize module with shared dependencies
----@param deps table Dependencies {ErrorHandler}
-function ScrollManager.init(deps)
- if type(deps) == "table" then
- ScrollManager._ErrorHandler = deps.ErrorHandler or ScrollManager._ErrorHandler
- ScrollManager._Context = deps.Context or ScrollManager._Context
- ScrollManager._StateManager = deps.StateManager or ScrollManager._StateManager
- end
-end
-
---- Create a new ScrollManager instance
----@param config table Configuration options
----@param deps table Dependencies {Color: Color module, utils: utils module}
----@return ScrollManager
-function ScrollManager.new(config, deps)
- local Color = deps.Color
- local self = setmetatable({}, ScrollManager)
-
- -- Store dependencies for instance methods
- self._Color = Color
- self._utils = deps.utils
-
- -- Configuration
- self.overflow = config.overflow or "hidden"
- self.overflowX = config.overflowX
- self.overflowY = config.overflowY
-
- -- Scrollbar appearance
- self.scrollbarWidth = config.scrollbarWidth or 12
- self.scrollbarColor = config.scrollbarColor or Color.new(0.5, 0.5, 0.5, 0.8)
- self.scrollbarTrackColor = config.scrollbarTrackColor or Color.new(0.2, 0.2, 0.2, 0.5)
- self.scrollbarRadius = config.scrollbarRadius or 6
- self.scrollbarPadding = config.scrollbarPadding or 2
- self.scrollSpeed = config.scrollSpeed or 20
- self.invertScroll = config.invertScroll or false
- self.scrollBarStyle = config.scrollBarStyle -- Theme scrollbar style name (nil = use default)
-
- -- scrollbarKnobOffset can be number or table {x, y} or {horizontal, vertical}
- -- Only normalize if actually provided (nil means use theme default)
- if config.scrollbarKnobOffset ~= nil then
- self.scrollbarKnobOffset = self._utils.normalizeOffsetTable(config.scrollbarKnobOffset, 0)
- else
- self.scrollbarKnobOffset = nil
- end
-
- -- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
- self.hideScrollbars = self._utils.normalizeBooleanTable(config.hideScrollbars, false)
-
- -- Scrollbar placement: "reserve-space" (default) or "overlay"
- self.scrollbarPlacement = config.scrollbarPlacement or "reserve-space"
-
- -- Scrollbar balance: when true, reserve space on both sides for visual balance
- self.scrollbarBalance = config.scrollbarBalance or false
-
- -- Touch scrolling configuration
- self.touchScrollEnabled = config.touchScrollEnabled ~= false -- Default true
- self.momentumScrollEnabled = config.momentumScrollEnabled ~= false -- Default true
- self.bounceEnabled = config.bounceEnabled ~= false -- Default true
- self.scrollFriction = config.scrollFriction or 0.95 -- Exponential decay per frame
- self.bounceStiffness = config.bounceStiffness or 0.2 -- Spring constant
- self.maxOverscroll = config.maxOverscroll or 100 -- pixels
-
- -- Internal overflow state
- self._overflowX = false
- self._overflowY = false
- self._contentWidth = 0
- self._contentHeight = 0
-
- -- Scroll state (can be restored from config in immediate mode)
- self._scrollX = config._scrollX or 0
- self._scrollY = config._scrollY or 0
- self._targetScrollX = nil
- self._targetScrollY = nil
- self._smoothScrollSpeed = 0.25 -- Interpolation speed (0-1, higher = faster)
- self.smoothScrollEnabled = config.smoothScrollEnabled or false -- Enable smooth wheel scrolling
- self._maxScrollX = 0
- self._maxScrollY = 0
-
- -- Scrollbar interaction state
- self._scrollbarHoveredVertical = false
- self._scrollbarHoveredHorizontal = false
- self._scrollbarDragging = false
- self._hoveredScrollbar = nil -- "vertical" or "horizontal"
- self._scrollbarDragOffset = 0 -- DEPRECATED: kept for backward compatibility
- self._dragStartMouseX = 0 -- Mouse X position when drag started
- self._dragStartMouseY = 0 -- Mouse Y position when drag started
- self._dragStartScrollX = 0 -- Scroll X position when drag started
- self._dragStartScrollY = 0 -- Scroll Y position when drag started
- self._scrollbarPressHandled = false
-
- -- Touch scrolling state
- self._touchScrolling = false
- self._scrollVelocityX = 0
- self._scrollVelocityY = 0
- self._momentumScrolling = false
- self._lastTouchTime = 0
- self._lastTouchX = 0
- self._lastTouchY = 0
-
- return self
-end
-
---- Get the space reserved for scrollbars (width and height reduction)
---- This is called BEFORE layout to reduce available space for children
----@param element Element The parent Element instance
----@return number reservedWidth, number reservedHeight
-function ScrollManager:getReservedSpace()
- if self.scrollbarPlacement ~= "reserve-space" then
- return 0, 0
- end
-
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
-
- local reservedWidth = 0
- local reservedHeight = 0
-
- -- Reserve space for vertical scrollbar if overflow mode requires it
- if (overflowY == "scroll" or overflowY == "auto") and not self.hideScrollbars.vertical then
- local scrollbarSpace = self.scrollbarWidth + (self.scrollbarPadding * 2)
- reservedWidth = self.scrollbarBalance and (scrollbarSpace * 2) or scrollbarSpace
- end
-
- -- Reserve space for horizontal scrollbar if overflow mode requires it
- if (overflowX == "scroll" or overflowX == "auto") and not self.hideScrollbars.horizontal then
- local scrollbarSpace = self.scrollbarWidth + (self.scrollbarPadding * 2)
- reservedHeight = self.scrollbarBalance and (scrollbarSpace * 2) or scrollbarSpace
- end
-
- return reservedWidth, reservedHeight
-end
-
---- Detect if content overflows container bounds
----@param element Element The parent Element instance
-function ScrollManager:detectOverflow(element)
- -- Reset overflow state
- self._overflowX = false
- self._overflowY = false
- self._contentWidth = element.width
- self._contentHeight = element.height
-
- -- Skip detection if overflow is visible (no clipping needed)
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
- if overflowX == "visible" and overflowY == "visible" then
- return
- end
-
- -- Calculate content bounds based on children
- if #element.children == 0 then
- return -- No children, no overflow
- end
-
- local maxX, maxY = 0, 0
-
- -- Content area starts after padding
- local contentX = element.x + element.padding.left
- local contentY = element.y + element.padding.top
-
- for _, child in ipairs(element.children) do
- -- Skip absolutely positioned children (they don't contribute to overflow)
- if not child._explicitlyAbsolute then
- -- Calculate child's margin box bounds relative to content area
- local childMarginRight = child.x - contentX + child:getBorderBoxWidth() + child.margin.right
- local childMarginBottom = child.y - contentY + child:getBorderBoxHeight() + child.margin.bottom
-
- -- Track the maximum extents (we ignore negative space from margins)
- maxX = math.max(maxX, childMarginRight)
- maxY = math.max(maxY, childMarginBottom)
- end
- end
-
- -- Calculate content dimensions
- self._contentWidth = maxX
- self._contentHeight = maxY
-
- -- Detect overflow (compare against content area, not total element size).
- -- element.width/height semantics depend on unit type:
- -- px units → border-box size (padding NOT yet subtracted)
- -- %, vh, vw → content size (padding already subtracted by LayoutEngine)
- -- auto → content size
- -- Using getBorderBoxWidth/Height() normalises both cases: border-box - padding = content.
- local containerWidth = element:getBorderBoxWidth() - element.padding.left - element.padding.right
- local containerHeight = element:getBorderBoxHeight() - element.padding.top - element.padding.bottom
-
- -- If scrollbarPlacement is "reserve-space", we need to subtract the reserved space
- -- because the layout already accounted for it, but element.width/height are still full size
- if self.scrollbarPlacement == "reserve-space" then
- local reservedWidth, reservedHeight = self:getReservedSpace()
- containerWidth = containerWidth - reservedWidth
- containerHeight = containerHeight - reservedHeight
- end
-
- self._overflowX = self._contentWidth > containerWidth
- self._overflowY = self._contentHeight > containerHeight
-
- -- Calculate maximum scroll bounds
- self._maxScrollX = math.max(0, self._contentWidth - containerWidth)
- self._maxScrollY = math.max(0, self._contentHeight - containerHeight)
-
- -- Clamp current scroll position to new bounds
- self._scrollX = self._utils.clamp(self._scrollX, 0, self._maxScrollX)
- self._scrollY = self._utils.clamp(self._scrollY, 0, self._maxScrollY)
-end
-
---- Set scroll position with bounds clamping
----@param x number? -- X scroll position (nil to keep current)
----@param y number? -- Y scroll position (nil to keep current)
-function ScrollManager:setScroll(x, y)
- if x ~= nil then
- self._scrollX = self._utils.clamp(x, 0, self._maxScrollX)
- end
- if y ~= nil then
- self._scrollY = self._utils.clamp(y, 0, self._maxScrollY)
- end
-end
-
---- Get current scroll position
----@return number scrollX, number scrollY
-function ScrollManager:getScroll()
- return self._scrollX, self._scrollY
-end
-
---- Scroll by delta amount
----@param dx number? -- X delta (nil for no change)
----@param dy number? -- Y delta (nil for no change)
-function ScrollManager:scrollBy(dx, dy)
- if dx then
- self._scrollX = self._utils.clamp(self._scrollX + dx, 0, self._maxScrollX)
- end
- if dy then
- self._scrollY = self._utils.clamp(self._scrollY + dy, 0, self._maxScrollY)
- end
-end
-
---- Get maximum scroll bounds
----@return number maxScrollX, number maxScrollY
-function ScrollManager:getMaxScroll()
- return self._maxScrollX, self._maxScrollY
-end
-
---- Get scroll percentage (0-1)
----@return number percentX, number percentY
-function ScrollManager:getScrollPercentage()
- local percentX = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
- local percentY = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
- return percentX, percentY
-end
-
---- Check if element has overflow
----@return boolean hasOverflowX, boolean hasOverflowY
-function ScrollManager:hasOverflow()
- return self._overflowX, self._overflowY
-end
-
---- Get content dimensions (including overflow)
----@return number contentWidth, number contentHeight
-function ScrollManager:getContentSize()
- return self._contentWidth, self._contentHeight
-end
-
---- Calculate scrollbar dimensions and positions
----@param element Element The parent Element instance
----@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
-function ScrollManager:calculateScrollbarDimensions(element)
- local result = {
- vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
- horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
- }
-
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
-
- -- Vertical scrollbar
- -- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
- if overflowY == "scroll" then
- -- Always show scrollbar for "scroll" mode
- result.vertical.visible = true
- result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
-
- if self._overflowY then
- -- Content overflows, calculate proper thumb size
- local contentRatio = element.height / math.max(self._contentHeight, element.height)
- result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
-
- -- Calculate thumb position based on scroll ratio
- local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
- local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
- result.vertical.thumbY = maxThumbY * scrollRatio
- else
- -- No overflow, thumb fills entire track
- result.vertical.thumbHeight = result.vertical.trackHeight
- result.vertical.thumbY = 0
- end
- elseif self._overflowY and overflowY == "auto" then
- -- Only show scrollbar when content actually overflows
- result.vertical.visible = true
- result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
-
- -- Calculate thumb height based on content ratio
- local contentRatio = element.height / math.max(self._contentHeight, element.height)
- result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
-
- -- Calculate thumb position based on scroll ratio
- local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
- local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
- result.vertical.thumbY = maxThumbY * scrollRatio
- end
-
- -- Horizontal scrollbar
- -- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
- if overflowX == "scroll" then
- -- Always show scrollbar for "scroll" mode
- result.horizontal.visible = true
- result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
-
- if self._overflowX then
- -- Content overflows, calculate proper thumb size
- local contentRatio = element.width / math.max(self._contentWidth, element.width)
- result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
-
- -- Calculate thumb position based on scroll ratio
- local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
- local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
- result.horizontal.thumbX = maxThumbX * scrollRatio
- else
- -- No overflow, thumb fills entire track
- result.horizontal.thumbWidth = result.horizontal.trackWidth
- result.horizontal.thumbX = 0
- end
- elseif self._overflowX and overflowX == "auto" then
- -- Only show scrollbar when content actually overflows
- result.horizontal.visible = true
- result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
-
- -- Calculate thumb width based on content ratio
- local contentRatio = element.width / math.max(self._contentWidth, element.width)
- result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
-
- -- Calculate thumb position based on scroll ratio
- local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
- local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
- result.horizontal.thumbX = maxThumbX * scrollRatio
- end
-
- return result
-end
-
---- Get scrollbar at mouse position
----@param element Element The parent Element instance
----@param mouseX number
----@param mouseY number
----@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
-function ScrollManager:getScrollbarAtPosition(element, mouseX, mouseY)
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
-
- if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
- return nil
- end
-
- local dims = self:calculateScrollbarDimensions(element)
- local x, y = element.x, element.y
- local w, h = element.width, element.height
-
- -- Check vertical scrollbar (only if not hidden)
- if dims.vertical.visible and not self.hideScrollbars.vertical then
- -- Position scrollbar within content area (x, y is border-box origin)
- local contentX = x + element.padding.left
- local contentY = y + element.padding.top
- local trackX = contentX + w - self.scrollbarWidth - self.scrollbarPadding
- local trackY = contentY + self.scrollbarPadding
- local trackW = self.scrollbarWidth
- local trackH = dims.vertical.trackHeight
-
- if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
- -- Check if over thumb
- local thumbY = trackY + dims.vertical.thumbY
- local thumbH = dims.vertical.thumbHeight
- if mouseY >= thumbY and mouseY <= thumbY + thumbH then
- return { component = "vertical", region = "thumb" }
- else
- return { component = "vertical", region = "track" }
- end
- end
- end
-
- -- Check horizontal scrollbar (only if not hidden)
- if dims.horizontal.visible and not self.hideScrollbars.horizontal then
- -- Position scrollbar within content area (x, y is border-box origin)
- local contentX = x + element.padding.left
- local contentY = y + element.padding.top
- local trackX = contentX + self.scrollbarPadding
- local trackY = contentY + h - self.scrollbarWidth - self.scrollbarPadding
- local trackW = dims.horizontal.trackWidth
- local trackH = self.scrollbarWidth
-
- if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
- -- Check if over thumb
- local thumbX = trackX + dims.horizontal.thumbX
- local thumbW = dims.horizontal.thumbWidth
- if mouseX >= thumbX and mouseX <= thumbX + thumbW then
- return { component = "horizontal", region = "thumb" }
- else
- return { component = "horizontal", region = "track" }
- end
- end
- end
-
- return nil
-end
-
---- Handle scrollbar mouse press
----@param element Element The parent Element instance
----@param mouseX number
----@param mouseY number
----@param button number
----@return boolean -- True if event was consumed
-function ScrollManager:handleMousePress(element, mouseX, mouseY, button)
- if button ~= 1 then
- return false
- end -- Only left click
-
- local scrollbar = self:getScrollbarAtPosition(element, mouseX, mouseY)
- if not scrollbar then
- return false
- end
-
- if scrollbar.region == "thumb" then
- -- Start dragging thumb - store start positions for relative movement tracking
- self._scrollbarDragging = true
- self._hoveredScrollbar = scrollbar.component
-
- -- Store drag start positions for relative movement calculation
- self._dragStartMouseX = mouseX
- self._dragStartMouseY = mouseY
- self._dragStartScrollX = self._scrollX
- self._dragStartScrollY = self._scrollY
-
- return true -- Event consumed
- elseif scrollbar.region == "track" then
- self:_scrollToTrackPosition(element, mouseX, mouseY, scrollbar.component)
- return true
- end
-
- return false
-end
-
---- Handle scrollbar drag
----@param element Element The parent Element instance
----@param mouseX number
----@param mouseY number
----@return boolean -- True if event was consumed
-function ScrollManager:handleMouseMove(element, mouseX, mouseY)
- if not self._scrollbarDragging then
- return false
- end
-
- local dims = self:calculateScrollbarDimensions(element)
-
- if self._hoveredScrollbar == "vertical" then
- local trackH = dims.vertical.trackHeight
- local thumbH = dims.vertical.thumbHeight
-
- -- Calculate relative mouse movement from drag start
- local mouseDeltaY = mouseY - self._dragStartMouseY
-
- -- Convert mouse delta to scroll delta
- -- scrollDelta / maxScroll = thumbDelta / (trackHeight - thumbHeight)
- local scrollableTrackHeight = trackH - thumbH
- local scrollDelta = scrollableTrackHeight > 0 and (mouseDeltaY / scrollableTrackHeight) * self._maxScrollY or 0
-
- local newScrollY = self._dragStartScrollY + scrollDelta
- newScrollY = self._utils.clamp(newScrollY, 0, self._maxScrollY)
-
- self:setScroll(nil, newScrollY)
- return true
- elseif self._hoveredScrollbar == "horizontal" then
- local trackW = dims.horizontal.trackWidth
- local thumbW = dims.horizontal.thumbWidth
-
- -- Calculate relative mouse movement from drag start
- local mouseDeltaX = mouseX - self._dragStartMouseX
-
- -- Convert mouse delta to scroll delta
- local scrollableTrackWidth = trackW - thumbW
- local scrollDelta = scrollableTrackWidth > 0 and (mouseDeltaX / scrollableTrackWidth) * self._maxScrollX or 0
-
- -- Apply delta to starting scroll position
- local newScrollX = self._dragStartScrollX + scrollDelta
- newScrollX = self._utils.clamp(newScrollX, 0, self._maxScrollX)
-
- self:setScroll(newScrollX, nil)
- return true
- end
-
- return false
-end
-
---- Handle scrollbar release
----@param button number
----@return boolean -- True if event was consumed
-function ScrollManager:handleMouseRelease(button)
- if button ~= 1 then
- return false
- end
-
- if self._scrollbarDragging then
- self._scrollbarDragging = false
- return true
- end
-
- return false
-end
-
---- Scroll to track click position (internal helper)
----@param element Element The parent Element instance
----@param mouseX number
----@param mouseY number
----@param component string -- "vertical" or "horizontal"
-function ScrollManager:_scrollToTrackPosition(element, mouseX, mouseY, component)
- local dims = self:calculateScrollbarDimensions(element)
-
- if component == "vertical" then
- local contentY = element.y + element.padding.top
- local trackY = contentY + self.scrollbarPadding
- local trackH = dims.vertical.trackHeight
- local thumbH = dims.vertical.thumbHeight
-
- -- Calculate target thumb position (centered on click)
- local targetThumbY = mouseY - trackY - (thumbH / 2)
- targetThumbY = self._utils.clamp(targetThumbY, 0, trackH - thumbH)
-
- -- Convert to scroll position
- local scrollRatio = (trackH - thumbH) > 0 and (targetThumbY / (trackH - thumbH)) or 0
- local newScrollY = scrollRatio * self._maxScrollY
-
- self:setScroll(nil, newScrollY)
- elseif component == "horizontal" then
- local contentX = element.x + element.padding.left
- local trackX = contentX + self.scrollbarPadding
- local trackW = dims.horizontal.trackWidth
- local thumbW = dims.horizontal.thumbWidth
-
- -- Calculate target thumb position (centered on click)
- local targetThumbX = mouseX - trackX - (thumbW / 2)
- targetThumbX = self._utils.clamp(targetThumbX, 0, trackW - thumbW)
-
- -- Convert to scroll position
- local scrollRatio = (trackW - thumbW) > 0 and (targetThumbX / (trackW - thumbW)) or 0
- local newScrollX = scrollRatio * self._maxScrollX
-
- self:setScroll(newScrollX, nil)
- end
-end
-
---- Handle mouse wheel scrolling
----@param x number -- Horizontal scroll amount
----@param y number -- Vertical scroll amount
----@return boolean -- True if scroll was handled
-function ScrollManager:handleWheel(x, y)
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
-
- if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
- return false
- end
-
- -- In immediate mode, overflow might not be calculated yet, so allow scrolling based on maxScroll values
- -- If _overflowY is nil/false but _maxScrollY > 0, we should still allow scrolling (from restored state)
- local hasVerticalOverflow = (self._overflowY and self._maxScrollY > 0) or (self._maxScrollY and self._maxScrollY > 0)
- local hasHorizontalOverflow = (self._overflowX and self._maxScrollX > 0)
- or (self._maxScrollX and self._maxScrollX > 0)
-
- local scrolled = false
-
- -- Vertical scrolling
- if y ~= 0 and (overflowY == "scroll" or overflowY == "auto") and hasVerticalOverflow then
- local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up
- if self.invertScroll then
- delta = -delta -- Invert scroll direction if enabled
- end
- if self.smoothScrollEnabled then
- -- Set target for smooth scrolling instead of instant jump
- self._targetScrollY = self._utils.clamp((self._targetScrollY or self._scrollY) + delta, 0, self._maxScrollY)
- else
- -- Instant scrolling (default behavior)
- local newScrollY = self._scrollY + delta
- self:setScroll(nil, newScrollY)
- end
- scrolled = true
- end
-
- -- Horizontal scrolling
- if x ~= 0 and (overflowX == "scroll" or overflowX == "auto") and hasHorizontalOverflow then
- local delta = -x * self.scrollSpeed
- if self.invertScroll then
- delta = -delta -- Invert scroll direction if enabled
- end
- if self.smoothScrollEnabled then
- -- Set target for smooth scrolling instead of instant jump
- self._targetScrollX = self._utils.clamp((self._targetScrollX or self._scrollX) + delta, 0, self._maxScrollX)
- else
- -- Instant scrolling (default behavior)
- local newScrollX = self._scrollX + delta
- self:setScroll(newScrollX, nil)
- end
- scrolled = true
- end
-
- return scrolled
-end
-
---- Update scrollbar hover state based on mouse position
----@param element Element The parent Element instance
----@param mouseX number
----@param mouseY number
-function ScrollManager:updateHoverState(element, mouseX, mouseY)
- local scrollbar = self:getScrollbarAtPosition(element, mouseX, mouseY)
-
- if scrollbar then
- if scrollbar.component == "vertical" then
- self._scrollbarHoveredVertical = true
- self._scrollbarHoveredHorizontal = false
- elseif scrollbar.component == "horizontal" then
- self._scrollbarHoveredVertical = false
- self._scrollbarHoveredHorizontal = true
- end
- else
- self._scrollbarHoveredVertical = false
- self._scrollbarHoveredHorizontal = false
- end
-end
-
---- Reset scrollbar press handled flag (call at start of frame)
-function ScrollManager:resetScrollbarPressFlag()
- self._scrollbarPressHandled = false
-end
-
---- Check if scrollbar press was handled this frame
----@return boolean
-function ScrollManager:wasScrollbarPressHandled()
- return self._scrollbarPressHandled
-end
-
---- Set scrollbar press handled flag
-function ScrollManager:setScrollbarPressHandled()
- self._scrollbarPressHandled = true
-end
-
---- Get state for immediate mode persistence
----@return table State data
-function ScrollManager:getState()
- return {
- _scrollX = self._scrollX or 0,
- _scrollY = self._scrollY or 0,
- _targetScrollX = self._targetScrollX,
- _targetScrollY = self._targetScrollY,
- _scrollbarDragging = self._scrollbarDragging or false,
- _hoveredScrollbar = self._hoveredScrollbar,
- _scrollbarDragOffset = self._scrollbarDragOffset or 0, -- Deprecated but kept for compatibility
- _dragStartMouseX = self._dragStartMouseX or 0,
- _dragStartMouseY = self._dragStartMouseY or 0,
- _dragStartScrollX = self._dragStartScrollX or 0,
- _dragStartScrollY = self._dragStartScrollY or 0,
- _scrollbarHoveredVertical = self._scrollbarHoveredVertical or false,
- _scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal or false,
- scrollBarStyle = self.scrollBarStyle,
- scrollbarKnobOffset = self.scrollbarKnobOffset,
- scrollbarPlacement = self.scrollbarPlacement,
- scrollbarBalance = self.scrollbarBalance,
- _overflowX = self._overflowX,
- _overflowY = self._overflowY,
- _contentWidth = self._contentWidth,
- _contentHeight = self._contentHeight,
- -- Touch fling state: without these, immediate-mode recreation zeroes the
- -- release velocity on the next frame and momentum scrolling never runs.
- _touchScrolling = self._touchScrolling or false,
- _momentumScrolling = self._momentumScrolling or false,
- _scrollVelocityX = self._scrollVelocityX or 0,
- _scrollVelocityY = self._scrollVelocityY or 0,
- _lastTouchTime = self._lastTouchTime or 0,
- _lastTouchX = self._lastTouchX or 0,
- _lastTouchY = self._lastTouchY or 0,
- }
-end
-
---- Set state from immediate mode persistence
----@param state table State data
-function ScrollManager:setState(state)
- if not state then
- return
- end
-
- -- Support both old (scrollX) and new (_scrollX) field names for backward compatibility
- if state._scrollX ~= nil then
- self._scrollX = state._scrollX
- elseif state.scrollX ~= nil then
- self._scrollX = state.scrollX
- end
-
- if state._scrollY ~= nil then
- self._scrollY = state._scrollY
- elseif state.scrollY ~= nil then
- self._scrollY = state.scrollY
- end
-
- if state._scrollbarDragging ~= nil then
- self._scrollbarDragging = state._scrollbarDragging
- elseif state.scrollbarDragging ~= nil then
- self._scrollbarDragging = state.scrollbarDragging
- end
-
- if state._hoveredScrollbar ~= nil then
- self._hoveredScrollbar = state._hoveredScrollbar
- elseif state.hoveredScrollbar ~= nil then
- self._hoveredScrollbar = state.hoveredScrollbar
- end
-
- if state._scrollbarDragOffset ~= nil then
- self._scrollbarDragOffset = state._scrollbarDragOffset
- elseif state.scrollbarDragOffset ~= nil then
- self._scrollbarDragOffset = state.scrollbarDragOffset
- end
-
- -- Restore drag start positions for relative movement tracking
- if state._dragStartMouseX ~= nil then
- self._dragStartMouseX = state._dragStartMouseX
- end
-
- if state._dragStartMouseY ~= nil then
- self._dragStartMouseY = state._dragStartMouseY
- end
-
- if state._dragStartScrollX ~= nil then
- self._dragStartScrollX = state._dragStartScrollX
- end
-
- if state._dragStartScrollY ~= nil then
- self._dragStartScrollY = state._dragStartScrollY
- end
-
- if state._scrollbarHoveredVertical ~= nil then
- self._scrollbarHoveredVertical = state._scrollbarHoveredVertical
- end
-
- if state._scrollbarHoveredHorizontal ~= nil then
- self._scrollbarHoveredHorizontal = state._scrollbarHoveredHorizontal
- end
-
- if state.scrollBarStyle ~= nil then
- self.scrollBarStyle = state.scrollBarStyle
- end
-
- if state.scrollbarKnobOffset ~= nil then
- self.scrollbarKnobOffset = self._utils.normalizeOffsetTable(state.scrollbarKnobOffset, 0)
- end
-
- if state.scrollbarPlacement ~= nil then
- self.scrollbarPlacement = state.scrollbarPlacement
- end
-
- if state.scrollbarBalance ~= nil then
- self.scrollbarBalance = state.scrollbarBalance
- end
-
- if state._overflowX ~= nil then
- self._overflowX = state._overflowX
- end
-
- if state._overflowY ~= nil then
- self._overflowY = state._overflowY
- end
-
- if state._contentWidth ~= nil then
- self._contentWidth = state._contentWidth
- end
-
- if state._contentHeight ~= nil then
- self._contentHeight = state._contentHeight
- end
-
- if state._targetScrollX ~= nil then
- self._targetScrollX = state._targetScrollX
- end
-
- if state._targetScrollY ~= nil then
- self._targetScrollY = state._targetScrollY
- end
-
- -- Touch fling state (see getState): restore so momentum survives
- -- immediate-mode element recreation between frames.
- if state._touchScrolling ~= nil then
- self._touchScrolling = state._touchScrolling
- end
- if state._momentumScrolling ~= nil then
- self._momentumScrolling = state._momentumScrolling
- end
- if state._scrollVelocityX ~= nil then
- self._scrollVelocityX = state._scrollVelocityX
- end
- if state._scrollVelocityY ~= nil then
- self._scrollVelocityY = state._scrollVelocityY
- end
- if state._lastTouchTime ~= nil then
- self._lastTouchTime = state._lastTouchTime
- end
- if state._lastTouchX ~= nil then
- self._lastTouchX = state._lastTouchX
- end
- if state._lastTouchY ~= nil then
- self._lastTouchY = state._lastTouchY
- end
-end
-
---- Handle touch press for scrolling
----@param touchX number
----@param touchY number
----@return boolean -- True if touch scroll started
-function ScrollManager:handleTouchPress(touchX, touchY)
- if not self.touchScrollEnabled then
- return false
- end
-
- local overflowX = self.overflowX or self.overflow
- local overflowY = self.overflowY or self.overflow
-
- if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
- return false
- end
-
- -- Stop momentum scrolling if active
- if self._momentumScrolling then
- self._momentumScrolling = false
- self._scrollVelocityX = 0
- self._scrollVelocityY = 0
- end
-
- -- Start touch scrolling
- self._touchScrolling = true
- self._lastTouchX = touchX
- self._lastTouchY = touchY
- self._lastTouchTime = love.timer.getTime()
-
- return true
-end
-
---- Handle touch move for scrolling
----@param touchX number
----@param touchY number
----@return boolean -- True if touch scroll was handled
-function ScrollManager:handleTouchMove(touchX, touchY)
- if not self._touchScrolling then
- return false
- end
-
- local currentTime = love.timer.getTime()
- local dt = currentTime - self._lastTouchTime
-
- if dt <= 0 then
- return false
- end
-
- -- Calculate delta and velocity
- local dx = touchX - self._lastTouchX
- local dy = touchY - self._lastTouchY
-
- -- Invert deltas (touch moves opposite to scroll)
- dx = -dx
- dy = -dy
-
- -- Calculate velocity (pixels per second)
- self._scrollVelocityX = dx / dt
- self._scrollVelocityY = dy / dt
-
- -- Apply scroll with bounce if enabled
- if self.bounceEnabled then
- -- Allow overscroll
- local newScrollX = self._scrollX + dx
- local newScrollY = self._scrollY + dy
-
- -- Clamp to max overscroll limits
- local minScrollX = -self.maxOverscroll
- local maxScrollX = self._maxScrollX + self.maxOverscroll
- local minScrollY = -self.maxOverscroll
- local maxScrollY = self._maxScrollY + self.maxOverscroll
-
- newScrollX = self._utils.clamp(newScrollX, minScrollX, maxScrollX)
- newScrollY = self._utils.clamp(newScrollY, minScrollY, maxScrollY)
-
- self._scrollX = newScrollX
- self._scrollY = newScrollY
- else
- -- Normal clamped scrolling
- self:scrollBy(dx, dy)
- end
-
- -- Update last touch state
- self._lastTouchX = touchX
- self._lastTouchY = touchY
- self._lastTouchTime = currentTime
-
- return true
-end
-
---- Handle touch release for scrolling
----@return boolean -- True if touch scroll was active
-function ScrollManager:handleTouchRelease()
- if not self._touchScrolling then
- return false
- end
-
- self._touchScrolling = false
-
- -- Start momentum scrolling if enabled and velocity is significant
- if self.momentumScrollEnabled then
- local velocityThreshold = 50 -- pixels per second
- local totalVelocity = math.sqrt(self._scrollVelocityX ^ 2 + self._scrollVelocityY ^ 2)
-
- if totalVelocity > velocityThreshold then
- self._momentumScrolling = true
- else
- self._scrollVelocityX = 0
- self._scrollVelocityY = 0
- end
- else
- self._scrollVelocityX = 0
- self._scrollVelocityY = 0
- end
-
- return true
-end
-
---- Update momentum scrolling (call every frame with dt)
----@param dt number Delta time in seconds
-function ScrollManager:update(dt)
- -- Smooth scroll interpolation
- if self._targetScrollX or self._targetScrollY then
- if self._targetScrollY then
- local diff = self._targetScrollY - self._scrollY
- if math.abs(diff) > 0.5 then
- self._scrollY = self._scrollY + diff * self._smoothScrollSpeed
- else
- self._scrollY = self._targetScrollY
- self._targetScrollY = nil
- end
- end
-
- if self._targetScrollX then
- local diff = self._targetScrollX - self._scrollX
- if math.abs(diff) > 0.5 then
- self._scrollX = self._scrollX + diff * self._smoothScrollSpeed
- else
- self._scrollX = self._targetScrollX
- self._targetScrollX = nil
- end
- end
- end
-
- if not self._momentumScrolling then
- -- Handle bounce back if overscrolled
- if self.bounceEnabled then
- self:_updateBounce(dt)
- end
- return
- end
-
- -- Apply velocity to scroll position
- local dx = self._scrollVelocityX * dt
- local dy = self._scrollVelocityY * dt
-
- if self.bounceEnabled then
- -- Allow overscroll during momentum
- self._scrollX = self._scrollX + dx
- self._scrollY = self._scrollY + dy
- else
- self:scrollBy(dx, dy)
- end
-
- -- Apply friction (exponential decay)
- self._scrollVelocityX = self._scrollVelocityX * self.scrollFriction
- self._scrollVelocityY = self._scrollVelocityY * self.scrollFriction
-
- -- Stop momentum when velocity is very low
- local totalVelocity = math.sqrt(self._scrollVelocityX ^ 2 + self._scrollVelocityY ^ 2)
- if totalVelocity < 1 then
- self._momentumScrolling = false
- self._scrollVelocityX = 0
- self._scrollVelocityY = 0
- end
-
- -- Handle bounce back if overscrolled
- if self.bounceEnabled then
- self:_updateBounce(dt)
- end
-end
-
---- Update bounce effect when overscrolled (internal)
----@param dt number Delta time in seconds
-function ScrollManager:_updateBounce()
- local bounced = false
-
- -- Bounce back horizontal overscroll
- if self._scrollX < 0 then
- local springForce = -self._scrollX * self.bounceStiffness
- self._scrollX = self._scrollX + springForce
- if math.abs(self._scrollX) < 0.5 then
- self._scrollX = 0
- end
- bounced = true
- elseif self._scrollX > self._maxScrollX then
- local overflow = self._scrollX - self._maxScrollX
- local springForce = -overflow * self.bounceStiffness
- self._scrollX = self._scrollX + springForce
- if math.abs(overflow) < 0.5 then
- self._scrollX = self._maxScrollX
- end
- bounced = true
- end
-
- -- Bounce back vertical overscroll
- if self._scrollY < 0 then
- local springForce = -self._scrollY * self.bounceStiffness
- self._scrollY = self._scrollY + springForce
- if math.abs(self._scrollY) < 0.5 then
- self._scrollY = 0
- end
- bounced = true
- elseif self._scrollY > self._maxScrollY then
- local overflow = self._scrollY - self._maxScrollY
- local springForce = -overflow * self.bounceStiffness
- self._scrollY = self._scrollY + springForce
- if math.abs(overflow) < 0.5 then
- self._scrollY = self._maxScrollY
- end
- bounced = true
- end
-
- -- Stop momentum if bouncing
- if bounced and self._momentumScrolling then
- -- Reduce velocity during bounce
- self._scrollVelocityX = self._scrollVelocityX * 0.9
- self._scrollVelocityY = self._scrollVelocityY * 0.9
- end
-end
-
---- Check if currently touch scrolling
----@return boolean
-function ScrollManager:isTouchScrolling()
- return self._touchScrolling
-end
-
---- Check if currently momentum scrolling
----@return boolean
-function ScrollManager:isMomentumScrolling()
- return self._momentumScrolling
-end
-
--------------------------------------------------------------------------------
--- Element-facing delegates
---
--- These wrappers bind the Element class's scroll API directly onto the
--- ScrollManager instance methods. Each takes the Element as its first argument
--- (the role `self` played when these methods lived on Element), performs the
--- nil-safety guard, invokes the owning ScrollManager instance, and syncs state
--- back onto the element for backward-compatible readers (Renderer, FlexLove,
--- Context hit-testing read these fields from Element).
---
--- Element binds them via direct assignment in Element.init, e.g.
--- Element.scrollToTop = Element._ScrollManager.scrollToTop
--- so Element retains only 1-line delegates and owns no scroll logic.
--------------------------------------------------------------------------------
-
-local _EMPTY_SCROLLBAR_DIMS = {
- vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
- horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
-}
-
---- Sync internal scroll state onto the element for backward-compatible readers.
----@param element table Element instance whose _scrollManager holds the state
-function ScrollManager.syncToElement(element)
- local sm = element._scrollManager
- if not sm then
- return
- end
- element._overflowX = sm._overflowX
- element._overflowY = sm._overflowY
- element._contentWidth = sm._contentWidth
- element._contentHeight = sm._contentHeight
- element._scrollX = sm._scrollX
- element._scrollY = sm._scrollY
- element._maxScrollX = sm._maxScrollX
- element._maxScrollY = sm._maxScrollY
- element._scrollbarHoveredVertical = sm._scrollbarHoveredVertical
- element._scrollbarHoveredHorizontal = sm._scrollbarHoveredHorizontal
- element._scrollbarDragging = sm._scrollbarDragging
- element._hoveredScrollbar = sm._hoveredScrollbar
- element._scrollbarDragOffset = sm._scrollbarDragOffset
-end
-
---- Backward-compatible alias retained by Element internals (update hover/drag).
-ScrollManager.syncScrollManagerState = ScrollManager.syncToElement
-
---- Detect overflow and sync state onto element.
----@param element table Element instance
-function ScrollManager._detectOverflow(element)
- local sm = element._scrollManager
- if not sm then
- return
- end
- sm:detectOverflow(element)
- ScrollManager.syncToElement(element)
-end
-
---- Set scroll position (element-facing). Nil args keep the current axis.
----@param element table Element instance
----@param x number? X scroll position
----@param y number? Y scroll position
-function ScrollManager.setScrollPosition(element, x, y)
- local sm = element._scrollManager
- if not sm then
- return
- end
- sm:setScroll(x, y)
- ScrollManager.syncToElement(element)
-end
-
---- Calculate scrollbar dimensions (element-facing).
----@param element table Element instance
----@return table dims {vertical, horizontal}
-function ScrollManager._calculateScrollbarDimensions(element)
- local sm = element._scrollManager
- if not sm then
- return _EMPTY_SCROLLBAR_DIMS
- end
- return sm:calculateScrollbarDimensions(element)
-end
-
---- Get scrollbar at mouse position (element-facing).
----@param element table Element instance
----@param mouseX number
----@param mouseY number
----@return table|nil {component, region}
-function ScrollManager._getScrollbarAtPosition(element, mouseX, mouseY)
- local sm = element._scrollManager
- if not sm then
- return nil
- end
- return sm:getScrollbarAtPosition(element, mouseX, mouseY)
-end
-
---- Handle scrollbar mouse press (element-facing).
----@param element table Element instance
----@param mouseX number
----@param mouseY number
----@param button number
----@return boolean consumed
-function ScrollManager._handleScrollbarPress(element, mouseX, mouseY, button)
- local sm = element._scrollManager
- if not sm then
- return false
- end
- local consumed = sm:handleMousePress(element, mouseX, mouseY, button)
- ScrollManager.syncToElement(element)
- return consumed
-end
-
---- Handle scrollbar drag (element-facing).
----@param element table Element instance
----@param mouseX number
----@param mouseY number
----@return boolean consumed
-function ScrollManager._handleScrollbarDrag(element, mouseX, mouseY)
- local sm = element._scrollManager
- if not sm then
- return false
- end
- local consumed = sm:handleMouseMove(element, mouseX, mouseY)
- ScrollManager.syncToElement(element)
- return consumed
-end
-
---- Handle scrollbar release (element-facing).
----@param element table Element instance
----@param button number
----@return boolean consumed
-function ScrollManager._handleScrollbarRelease(element, button)
- local sm = element._scrollManager
- if not sm then
- return false
- end
- local consumed = sm:handleMouseRelease(button)
- ScrollManager.syncToElement(element)
- return consumed
-end
-
---- Handle mouse wheel scrolling (element-facing).
----@param element table Element instance
----@param x number Horizontal scroll amount
----@param y number Vertical scroll amount
----@return boolean consumed
-function ScrollManager._handleWheelScroll(element, x, y)
- local sm = element._scrollManager
- if not sm then
- return false
- end
- local consumed = sm:handleWheel(x, y)
- ScrollManager.syncToElement(element)
- return consumed
-end
-
---- Get current scroll position (element-facing).
----@param element table Element instance
----@return number scrollX, number scrollY
-function ScrollManager.getScrollPosition(element)
- local sm = element._scrollManager
- if not sm then
- return 0, 0
- end
- return sm:getScroll()
-end
-
--- The following getters share names with ScrollManager *instance* methods,
--- so the element-facing wrappers use distinct `element`-prefixed names to
--- avoid shadowing the instance API (tests call sm:getMaxScroll() etc.).
-
---- Get maximum scroll bounds (element-facing).
----@param element table Element instance
----@return number maxScrollX, number maxScrollY
-function ScrollManager.elementGetMaxScroll(element)
- local sm = element._scrollManager
- if not sm then
- return 0, 0
- end
- return sm:getMaxScroll()
-end
-
---- Get scroll percentage 0-1 (element-facing).
----@param element table Element instance
----@return number percentX, number percentY
-function ScrollManager.elementGetScrollPercentage(element)
- local sm = element._scrollManager
- if not sm then
- return 0, 0
- end
- return sm:getScrollPercentage()
-end
-
---- Check if element has overflow (element-facing).
----@param element table Element instance
----@return boolean hasOverflowX, boolean hasOverflowY
-function ScrollManager.elementHasOverflow(element)
- local sm = element._scrollManager
- if not sm then
- return false, false
- end
- return sm:hasOverflow()
-end
-
---- Get content dimensions (element-facing).
----@param element table Element instance
----@return number contentWidth, number contentHeight
-function ScrollManager.elementGetContentSize(element)
- local sm = element._scrollManager
- if not sm then
- return 0, 0
- end
- return sm:getContentSize()
-end
-
---- Scroll by relative delta (element-facing).
--- In immediate mode, per-axis deltas whose scroll bound is still 0 are deferred
--- until layout calculates the bound (delegates to Element:_deferMethod).
----@param element table Element instance
----@param dx number? X delta
----@param dy number? Y delta
-function ScrollManager.elementScrollBy(element, dx, dy)
- local sm = element._scrollManager
- if not sm then
- return
- end
- local maxScrollX, maxScrollY = sm:getMaxScroll()
- if dx ~= nil and maxScrollX == 0 then
- element:_deferMethod("scrollBy", dx, nil)
- dx = nil
- end
- if dy ~= nil and maxScrollY == 0 then
- element:_deferMethod("scrollBy", nil, dy)
- dy = nil
- end
- if dx ~= nil or dy ~= nil then
- sm:scrollBy(dx, dy)
- ScrollManager.syncToElement(element)
- end
-end
-
---- Jump to the top of scrollable content.
----@param element table Element instance
-function ScrollManager.scrollToTop(element)
- element:setScrollPosition(nil, 0)
-end
-
---- Jump to the bottom of scrollable content.
--- Defers until layout has calculated the vertical scroll bound.
----@param element table Element instance
-function ScrollManager.scrollToBottom(element)
- local sm = element._scrollManager
- if not sm then
- return
- end
- local _, maxScrollY = sm:getMaxScroll()
- if maxScrollY > 0 then
- element:setScrollPosition(nil, maxScrollY)
- else
- element:_deferMethod("scrollToBottom")
- end
-end
-
---- Jump to the leftmost position of scrollable content.
----@param element table Element instance
-function ScrollManager.scrollToLeft(element)
- element:setScrollPosition(0, nil)
-end
-
---- Jump to the rightmost position of scrollable content.
--- Defers until layout has calculated the horizontal scroll bound.
----@param element table Element instance
-function ScrollManager.scrollToRight(element)
- local sm = element._scrollManager
- if not sm then
- return
- end
- local maxScrollX, _ = sm:getMaxScroll()
- if maxScrollX > 0 then
- element:setScrollPosition(maxScrollX, nil)
- else
- element:_deferMethod("scrollToRight")
- end
-end
-
---- Restore scrollbar state from StateManager in immediate mode.
----@param element table Element instance
-function ScrollManager.restoreImmediateState(element)
- -- Mode-aware guard: only immediate-mode frames keep state in StateManager;
- -- in retained mode the element (and its ScrollManager) persist between
- -- frames, so there is nothing to restore. Routed through StateManager so no
- -- raw mode check lives here (behavior-mode-unification task 11).
- if not element._stateId or not ScrollManager._StateManager.isImmediateMode() then
- return
- end
- local state = ScrollManager._StateManager.getState(element._stateId)
- if not state or not state.scrollManager then
- return
- end
- local sm_state = state.scrollManager
- element._scrollbarHoveredVertical = sm_state._scrollbarHoveredVertical or false
- element._scrollbarHoveredHorizontal = sm_state._scrollbarHoveredHorizontal or false
- element._scrollbarDragging = sm_state._scrollbarDragging or false
- element._hoveredScrollbar = sm_state._hoveredScrollbar
- element._scrollbarDragOffset = sm_state._scrollbarDragOffset or 0
-
- local sm = element._scrollManager
- if sm then
- sm._scrollbarHoveredVertical = element._scrollbarHoveredVertical
- sm._scrollbarHoveredHorizontal = element._scrollbarHoveredHorizontal
- sm._scrollbarDragging = element._scrollbarDragging
- sm._hoveredScrollbar = element._hoveredScrollbar
- sm._scrollbarDragOffset = element._scrollbarDragOffset
- sm._dragStartMouseX = sm_state._dragStartMouseX or 0
- sm._dragStartMouseY = sm_state._dragStartMouseY or 0
- sm._dragStartScrollX = sm_state._dragStartScrollX or 0
- sm._dragStartScrollY = sm_state._dragStartScrollY or 0
- end
-end
-
---- Update hover, drag, and press interaction for scrollbars during Element:update.
----@param element table Element instance
----@param mx number Mouse X
----@param my number Mouse Y
-function ScrollManager.updateInteraction(element, mx, my)
- local sm = element._scrollManager
- if sm then
- sm:updateHoverState(element, mx, my)
- ScrollManager.syncToElement(element)
- end
-
- if element._scrollbarDragging and love.mouse.isDown(1) then
- ScrollManager._handleScrollbarDrag(element, mx, my)
- elseif element._scrollbarDragging then
- if sm then
- sm:handleMouseRelease(1)
- ScrollManager.syncToElement(element)
- end
- if element._stateId and ScrollManager._StateManager.isImmediateMode() then
- ScrollManager._StateManager.updateState(element._stateId, {
- scrollbarDragging = false,
- })
- end
- end
-
- -- Handle scrollbar press for elements with scrollable overflow
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- local hasScrollableOverflow = (
- overflowX == "scroll"
- or overflowX == "auto"
- or overflowY == "scroll"
- or overflowY == "auto"
- )
-
- if hasScrollableOverflow and not element._scrollbarDragging then
- if love.mouse.isDown(1) and not element._scrollbarPressHandled then
- local scrollbarPressed = ScrollManager._handleScrollbarPress(element, mx, my, 1)
- if scrollbarPressed then
- element._scrollbarPressHandled = true
- end
- elseif not love.mouse.isDown(1) then
- element._scrollbarPressHandled = false
- end
- end
-end
-
-return ScrollManager
diff --git a/libs/flexlove/modules/Select.lua b/libs/flexlove/modules/Select.lua
deleted file mode 100644
index 21de8ecd..00000000
--- a/libs/flexlove/modules/Select.lua
+++ /dev/null
@@ -1,719 +0,0 @@
----@class Select
-local Select = {}
-
----Initialize Select module with required dependencies
----@param deps table
-function Select.init(deps)
- Select._ErrorHandler = deps.ErrorHandler
- Select._Context = deps.Context
- Select._StateManager = deps.StateManager
- Select._utils = deps.utils
- Select._Element = deps.Element
-end
-
----Initialize selectParent state on an element
----@param element Element
----@param selectParentConfig table
-function Select.initSelectParent(element, selectParentConfig)
- element._selectState = {
- value = selectParentConfig.value,
- open = selectParentConfig.open or false,
- placeholder = selectParentConfig.placeholder,
- selectFrame = nil,
- selectAnchor = nil,
- onChange = selectParentConfig.onChange,
- options = {},
- optionLookup = {},
- expectedFrameParent = nil,
- frameAdopted = false,
- }
-
- -- Restore select state from StateManager. Mode-aware via
- -- Context.isImmediateMode (behavior-mode-unification task 11).
- if Select._Context.isImmediateMode() and element._stateId and element._stateId ~= "" then
- local state = Select._StateManager.getState(element._stateId)
- if state and state._selectOpen ~= nil then
- element._selectState.open = state._selectOpen
- end
- if state and state._selectValue ~= nil then
- element._selectState.value = state._selectValue
- if element.selectParent then
- element.selectParent.value = state._selectValue
- end
- end
- if state and state._selectSelectedLabel ~= nil then
- element._selectState.selectedLabel = state._selectSelectedLabel
- end
- end
-end
-
----Initialize selectOption on an element
----@param element Element
----@param selectOptionConfig table
-function Select.initSelectOption(element, selectOptionConfig)
- element.selectOption = {
- value = selectOptionConfig.value,
- label = selectOptionConfig.label or element.text,
- disabled = selectOptionConfig.disabled or false,
- }
-end
-
----@param selectParent Element
-function Select.rebuildOptionLookup(selectParent)
- if not selectParent or not selectParent._selectState then
- return
- end
-
- selectParent._selectState.optionLookup = {}
- for _, optionElement in ipairs(selectParent._selectState.options) do
- if optionElement and optionElement.selectOption then
- selectParent._selectState.optionLookup[optionElement.selectOption.value] = optionElement
- end
- end
-end
-
----@param selectParent Element
-function Select.syncOptionStates(selectParent)
- if not selectParent or not selectParent._selectState then
- return
- end
-
- local selectedOption = nil
- local selectedLabel = selectParent._selectState.selectedLabel
-
- for _, optionElement in ipairs(selectParent._selectState.options) do
- local isSelected = optionElement.selectOption
- and optionElement.selectOption.value == selectParent._selectState.value
- optionElement._selectSelected = isSelected
- optionElement.ariaChecked = isSelected
-
- if isSelected then
- selectedOption = optionElement
- selectedLabel = optionElement.selectOption.label or optionElement.text
- end
- end
-
- selectParent._selectState.selectedOption = selectedOption
- selectParent._selectState.selectedLabel = selectedLabel
-end
-
----@param element Element
-function Select.resetOptions(element)
- if not element._selectState then
- return
- end
-
- element._selectState.options = {}
- element._selectState.optionLookup = {}
- element._selectState.selectedOption = nil
-end
-
----@param frame any
----@return boolean
-function Select.isValidSelectFrame(frame)
- local Element = Select._Element
- return type(frame) == "table" and getmetatable(frame) == Element
-end
-
----@param element Element
----@param code string
----@param details table?
-function Select.warnSelectFrame(element, code, details)
- Select._ErrorHandler:warn("Element", code, details or { element = element.id })
-end
-
----@param element Element
----@param frame Element
-function Select.trackManagedFrame(element, frame)
- element._selectState.selectFrame = frame
- local expectedParent = element._selectState.selectAnchor or element
- element._selectState.expectedFrameParent = expectedParent
- element._selectState.frameAdopted = frame.parent == expectedParent
- if frame._managedSelectBaseOpacity == nil then
- frame._managedSelectBaseOpacity = frame.opacity
- end
- if frame._managedSelectBaseVisibility == nil then
- frame._managedSelectBaseVisibility = frame.visibility or "visible"
- end
- if frame._managedSelectBaseDisabled == nil then
- frame._managedSelectBaseDisabled = frame.disabled or false
- end
- frame._managedSelectOwner = element
- frame._managedSelectFrame = true
-end
-
----@param element Element
----@return Element
-function Select.getOrCreateManagedAnchor(element)
- if element._selectState.selectAnchor then
- return element._selectState.selectAnchor
- end
-
- local Element = Select._Element
- local anchor = Element.new({
- id = string.format("%s__select_anchor", element.id or "select"),
- parent = element,
- positioning = Select._utils.enums.Positioning.ABSOLUTE,
- left = 0,
- top = element:getBorderBoxHeight(),
- width = element:getBorderBoxWidth(),
- opacity = 1,
- visibility = "hidden",
- disabled = true,
- })
-
- anchor._managedSelectAnchor = true
- anchor._managedSelectOwner = element
- element._selectState.selectAnchor = anchor
- return anchor
-end
-
----@param element Element
----@param frame Element
-function Select.applyManagedFrameLayout(element, frame)
- local anchor = Select.getOrCreateManagedAnchor(element)
- local triggerBorderBoxWidth = element:getBorderBoxWidth()
- anchor.left = 0
- anchor.top = element:getBorderBoxHeight()
- anchor.width = triggerBorderBoxWidth
- anchor.units.left = { value = 0, unit = "px" }
- anchor.units.top = { value = element:getBorderBoxHeight(), unit = "px" }
- anchor.units.width = { value = triggerBorderBoxWidth, unit = "px" }
- frame._managedSelectMinimumBorderBoxWidth = triggerBorderBoxWidth
-
- frame.positioning = frame.positioning or Select._utils.enums.Positioning.RELATIVE
- frame._explicitlyAbsolute = false
- frame.left = nil
- frame.top = nil
- frame.right = nil
- frame.bottom = nil
-
- if frame.parent ~= anchor then
- frame:setParent(anchor)
- end
-
- if frame.autosizing and frame.autosizing.width then
- local contentWidth = frame:calculateAutoWidth()
- frame._borderBoxWidth = contentWidth + frame.padding.left + frame.padding.right
- frame.width = contentWidth
- end
-
- if frame.parent == anchor then
- anchor.width = math.max(triggerBorderBoxWidth, frame:getBorderBoxWidth())
- anchor.units.width = { value = anchor.width, unit = "px" }
- end
-
- element._selectState.expectedFrameParent = anchor
- element._selectState.frameAdopted = frame.parent == anchor
-end
-
----@param element Element
----@param frame Element
-function Select.adoptSelectFrame(element, frame)
- if not element._selectState then
- return
- end
-
- if not Select.isValidSelectFrame(frame) then
- Select.warnSelectFrame(element, "ELEM_007", {
- element = element.id,
- property = "selectParent.selectFrame",
- got = type(frame),
- })
- return
- end
-
- if frame == element then
- Select.warnSelectFrame(element, "ELEM_007", {
- element = element.id,
- property = "selectParent.selectFrame",
- reason = "select cannot use itself as its managed frame",
- })
- return
- end
-
- local anchor = Select.getOrCreateManagedAnchor(element)
-
- if frame.parent and frame.parent ~= element and frame.parent ~= anchor then
- Select.warnSelectFrame(element, "ELEM_008", {
- element = element.id,
- frame = frame.id,
- parent = frame.parent.id,
- })
- end
-
- Select.trackManagedFrame(element, frame)
- Select.applyManagedFrameLayout(element, frame)
- Select.syncManagedFrameVisibility(element)
-
- -- Layout is deferred to endFrame in immediate mode. shouldLayout()
- -- encapsulates the mode check (behavior-mode-unification task 11).
- if Select._StateManager.shouldLayout() then
- anchor:layoutChildren()
- element:layoutChildren()
- end
-
- local pendingOptions = {}
- for _, child in ipairs(element.children) do
- if child ~= frame and child.selectOption then
- table.insert(pendingOptions, child)
- end
- end
-
- for _, option in ipairs(pendingOptions) do
- Select.attachOptionToManagedFrame(option)
- end
-end
-
----@param element Element
-function Select.ensureFrameState(element)
- if not element._selectState or not element._selectState.selectFrame then
- return
- end
-
- local frame = element._selectState.selectFrame
- local anchor = element._selectState.selectAnchor
- if anchor then
- local triggerBorderBoxWidth = element:getBorderBoxWidth()
- anchor.left = 0
- anchor.top = element:getBorderBoxHeight()
- anchor.width = triggerBorderBoxWidth
- anchor.units.left = { value = 0, unit = "px" }
- anchor.units.top = { value = element:getBorderBoxHeight(), unit = "px" }
- anchor.units.width = { value = triggerBorderBoxWidth, unit = "px" }
- frame._managedSelectMinimumBorderBoxWidth = triggerBorderBoxWidth
- if frame.autosizing and frame.autosizing.width then
- local contentWidth = frame:calculateAutoWidth()
- frame._borderBoxWidth = contentWidth + frame.padding.left + frame.padding.right
- frame.width = contentWidth
- end
- if frame.parent == anchor then
- anchor.width = math.max(triggerBorderBoxWidth, frame:getBorderBoxWidth())
- anchor.units.width = { value = anchor.width, unit = "px" }
- end
- if frame.parent == anchor then
- anchor:layoutChildren()
- end
- elseif frame.parent == element then
- Select.applyManagedFrameLayout(element, frame)
- end
-
- local expectedParent = anchor or element._selectState.expectedFrameParent
- if frame.parent ~= expectedParent then
- Select.warnSelectFrame(element, "ELEM_009", {
- element = element.id,
- frame = frame.id,
- expectedParent = expectedParent and expectedParent.id or nil,
- actualParent = frame.parent and frame.parent.id or nil,
- })
- element._selectState.expectedFrameParent = frame.parent
- element._selectState.frameAdopted = frame.parent == expectedParent
- end
-end
-
----@param element Element
-function Select.syncManagedFrameVisibility(element)
- if not element._selectState or not element._selectState.selectFrame then
- return
- end
-
- local frame = element._selectState.selectFrame
- local anchor = element._selectState.selectAnchor
- local isOpen = element._selectState.open == true
- frame.visibility = isOpen and (frame._managedSelectBaseVisibility or "visible") or "hidden"
- frame.opacity = frame._managedSelectBaseOpacity or 1
- if isOpen then
- frame.disabled = frame._managedSelectBaseDisabled == true
- else
- frame.disabled = true
- end
- if anchor then
- anchor.visibility = isOpen and "visible" or "hidden"
- anchor.opacity = 1
- anchor.disabled = not isOpen
- end
-end
-
----@param element Element
----@return Element?
-function Select.findOwningSelectParent(element)
- if element._selectParentHint and element._selectParentHint._selectState then
- return element._selectParentHint
- end
-
- local current = element.parent
- while current do
- if current._selectState then
- return current
- end
- current = current.parent
- end
-
- return nil
-end
-
----@param element Element
-function Select.registerWithSelectParent(element)
- if not element.selectOption then
- return
- end
-
- local selectParent = Select.findOwningSelectParent(element)
- if not selectParent then
- return
- end
-
- element._selectParentElement = selectParent
-
- for _, optionElement in ipairs(selectParent._selectState.options) do
- if optionElement == element then
- return
- end
- end
-
- table.insert(selectParent._selectState.options, element)
- Select.rebuildOptionLookup(selectParent)
- Select.syncOptionStates(selectParent)
-end
-
----@param element Element
-function Select.attachOptionToManagedFrame(element)
- if not element.selectOption then
- return
- end
-
- local selectParent = Select.findOwningSelectParent(element)
- if not selectParent or not selectParent._selectState or not selectParent._selectState.selectFrame then
- return
- end
-
- local selectFrame = selectParent._selectState.selectFrame
- if element.parent ~= selectFrame then
- element._selectParentHint = selectParent
-
- if
- element._originalPositioning == Select._utils.enums.Positioning.ABSOLUTE
- and element._managedSelectOptionUsesFrameLayout == nil
- then
- element._managedSelectOptionUsesFrameLayout = true
- element.positioning = Select._utils.enums.Positioning.RELATIVE
- element._originalPositioning = nil
- element._explicitlyAbsolute = false
- element.left = nil
- element.top = nil
- element.right = nil
- element.bottom = nil
- end
-
- element:setParent(selectFrame)
- -- Ensure frame geometry eagerly only in retained mode; deferred to the
- -- per-frame update in immediate mode (behavior-mode-unification task 11).
- if Select._StateManager.shouldLayout() then
- Select.ensureFrameState(selectParent)
- end
- end
-end
-
----@param element Element
-function Select.unregisterFromSelectParent(element)
- if not element.selectOption or not element._selectParentElement or not element._selectParentElement._selectState then
- element._selectParentElement = nil
- return
- end
-
- local selectParent = element._selectParentElement
- for index, optionElement in ipairs(selectParent._selectState.options) do
- if optionElement == element then
- table.remove(selectParent._selectState.options, index)
- break
- end
- end
-
- Select.rebuildOptionLookup(selectParent)
- Select.syncOptionStates(selectParent)
- element._selectParentElement = nil
-end
-
----@param element Element
-function Select.saveStateToStateManager(element)
- if not element._selectState then
- return
- end
- if element._stateId and Select._Context.isImmediateMode() and element._stateId ~= "" then
- Select._StateManager.updateState(element._stateId, {
- _selectOpen = element._selectState.open,
- _selectValue = element._selectState.value,
- _selectSelectedLabel = element._selectState.selectedLabel,
- })
- end
-end
-
----@param element Element
-function Select.openSelect(element)
- if not element._selectState then
- return
- end
-
- Select.ensureFrameState(element)
- element._selectState.open = true
- element.ariaExpanded = true
- if element.selectParent then
- element.selectParent.open = true
- end
- Select.syncManagedFrameVisibility(element)
- Select.saveStateToStateManager(element)
-end
-
----@param element Element
-function Select.closeSelect(element)
- if not element._selectState then
- return
- end
-
- Select.ensureFrameState(element)
- element._selectState.open = false
- element.ariaExpanded = false
- if element.selectParent then
- element.selectParent.open = false
- end
- Select.syncManagedFrameVisibility(element)
- Select.saveStateToStateManager(element)
-end
-
----@param element Element
-function Select.toggleSelect(element)
- if not element._selectState then
- return
- end
-
- if element.disabled then
- return
- end
-
- if element._selectState.open then
- Select.closeSelect(element)
- else
- Select.openSelect(element)
- end
-
- if element.onEvent then
- element.onEvent(element, { type = "selecttoggle", open = element._selectState.open })
- end
-end
-
----@param element Element
----@return boolean
-function Select.isSelectOpen(element)
- return element._selectState ~= nil and element._selectState.open == true
-end
-
----@param element Element
----@return any
-function Select.getSelectValue(element)
- if not element._selectState then
- return nil
- end
- return element._selectState.value
-end
-
----@param element Element
----@return string?
-function Select.getSelectLabel(element)
- if not element._selectState then
- return nil
- end
-
- local selectedOption = element._selectState.selectedOption
- or element._selectState.optionLookup[element._selectState.value]
- if selectedOption and selectedOption.selectOption then
- return selectedOption.selectOption.label or selectedOption.text
- end
-
- return element._selectState.selectedLabel or element._selectState.placeholder
-end
-
----@param element Element
----@return boolean
-function Select.isSelectedOption(element)
- if not element.selectOption or not element._selectParentElement or not element._selectParentElement._selectState then
- return false
- end
- return element._selectParentElement._selectState.value == element.selectOption.value
-end
-
----@param element Element
----@param value any
----@param optionElement Element?
-function Select.setSelectValue(element, value, optionElement)
- if not element._selectState then
- return
- end
-
- if element.disabled then
- return
- end
-
- local didChange = element._selectState.value ~= value
- element._selectState.value = value
- if element.selectParent then
- element.selectParent.value = value
- end
-
- if optionElement and optionElement.selectOption then
- element._selectState.selectedLabel = optionElement.selectOption.label or optionElement.text
- end
-
- Select.syncOptionStates(element)
- Select.closeSelect(element)
- Select.saveStateToStateManager(element)
-
- if element.onEvent then
- element.onEvent(element, { type = "selectchange", value = value, option = optionElement })
- end
-
- if didChange and element._selectState.onChange then
- element._selectState.onChange(element, value, optionElement and optionElement.selectOption or nil)
- end
-end
-
----@param element Element
-function Select.handleRelease(element)
- if element.disabled then
- return
- end
-
- if element.selectOption then
- local selectParent = element._selectParentElement or Select.findOwningSelectParent(element)
- if not selectParent then
- return
- end
-
- if element.selectOption.disabled then
- Select.closeSelect(selectParent)
- return
- end
-
- Select.setSelectValue(selectParent, element.selectOption.value, element)
- return
- end
-
- if element._selectState then
- Select.toggleSelect(element)
- end
-end
-
----Save select state for state persistence (called from Element:saveState)
----@param element Element
----@return table?
-function Select.saveState(element)
- if not element._selectState then
- return nil
- end
- return {
- value = element._selectState.value,
- open = element._selectState.open,
- selectedLabel = element._selectState.selectedLabel,
- }
-end
-
----Restore select state (called from Element:restoreState)
----@param element Element
----@param state table
-function Select.restoreState(element, state)
- if not element._selectState or not state then
- return
- end
- element._selectState.value = state.value
- element._selectState.open = state.open or false
- element._selectState.selectedLabel = state.selectedLabel
- if element.selectParent then
- element.selectParent.value = state.value
- element.selectParent.open = state.open or false
- end
- element.ariaExpanded = element._selectState.open
- Select.syncOptionStates(element)
-end
-
----Clean up select-related resources (called from Element:destroy)
----@param element Element
-function Select.cleanupDestroy(element)
- if element._selectState then
- local frame = element._selectState.selectFrame
- local anchor = element._selectState.selectAnchor
- if frame then
- frame._managedSelectOwner = nil
- frame._managedSelectFrame = nil
- frame._managedSelectBaseOpacity = nil
- frame._managedSelectBaseVisibility = nil
- frame._managedSelectBaseDisabled = nil
- end
- if anchor then
- anchor._managedSelectOwner = nil
- anchor._managedSelectAnchor = nil
- end
- element._selectState = nil
- end
- if element._managedSelectFrame and element._managedSelectOwner then
- if element._managedSelectOwner._selectState then
- element._managedSelectOwner._selectState.selectFrame = nil
- element._managedSelectOwner._selectState.expectedFrameParent = nil
- element._managedSelectOwner._selectState.frameAdopted = false
- end
- element._managedSelectOwner = nil
- element._managedSelectFrame = nil
- element._managedSelectBaseOpacity = nil
- element._managedSelectBaseVisibility = nil
- element._managedSelectBaseDisabled = nil
- end
- if element._managedSelectAnchor and element._managedSelectOwner then
- if element._managedSelectOwner._selectState then
- element._managedSelectOwner._selectState.selectAnchor = nil
- end
- element._managedSelectOwner = nil
- element._managedSelectAnchor = nil
- end
- if element.selectParent then
- element.selectParent.onChange = nil
- end
-end
-
---- Called when a select parent removes a child: clears frame/anchor refs if the removed child was the
---- select-managed frame or anchor. Keeps select state-mutation logic owned by the Select module.
----@param element Element The select parent whose child was removed.
----@param child Element The removed child.
-function Select.handleChildRemoved(element, child)
- if not element._selectState then
- return
- end
- if element._selectState.selectFrame == child then
- element._selectState.selectFrame = nil
- element._selectState.expectedFrameParent = nil
- element._selectState.frameAdopted = false
- end
- if element._selectState.selectAnchor == child then
- element._selectState.selectAnchor = nil
- end
-end
-
---- Layout-path hook: adjust an auto-width child's border-box width for a managed-select frame.
---- Invoked from LayoutEngine (via the Element delegate) during vertical-flex auto-width calculation.
----@param element Element The managed-select frame (the dropdown container).
----@param child Element The flex child being measured.
----@param childBorderBoxWidth number Current computed border-box width of `child`.
----@return number Possibly-adjusted border-box width.
-function Select.adjustAutoWidthChild(element, child, childBorderBoxWidth)
- if
- element._managedSelectFrame
- and element.autosizing
- and element.autosizing.width
- and child.units
- and child.units.width
- and child.units.width.unit == "%"
- then
- local intrinsicBorderBoxWidth = child:calculateAutoWidth() + child.padding.left + child.padding.right
- return math.max(childBorderBoxWidth, intrinsicBorderBoxWidth)
- end
- return childBorderBoxWidth
-end
-
-return Select
diff --git a/libs/flexlove/modules/StateManager.lua b/libs/flexlove/modules/StateManager.lua
deleted file mode 100644
index 3a7b2af7..00000000
--- a/libs/flexlove/modules/StateManager.lua
+++ /dev/null
@@ -1,790 +0,0 @@
----@class StateManager
-local StateManager = {}
-
--- ErrorHandler will be injected via init
-local ErrorHandler
-
--- State storage: ID -> state table
-local stateStore = {}
-
--- Frame tracking metadata: ID -> {lastFrame, createdFrame, accessCount}
-local stateMetadata = {}
-
--- Frame counter
-local frameNumber = 0
-
--- Counter to track multiple elements created at the same source location (e.g., in loops)
-local callSiteCounters = {}
-
--- Stateful element mapping: stateId -> element instance
--- Used in retained mode for cache-through: StateManager resolves id -> element -> field
-local statefulElements = {}
-
--- Dirty state tracking for flushFrame: set of {id, key} pairs modified this frame
-local dirtyState = {}
-
--- Immediate mode flag
-local _immediateMode = false
-
--- Configuration
-local config = {
- stateRetentionFrames = 2, -- Keep unused state for 2 frames
- maxStateEntries = 1000, -- Maximum state entries before forced GC
-}
-
--- Default state values (sparse storage - don't store these)
-local stateDefaults = {
- -- Interaction states
- hover = false,
- pressed = false,
- focused = false,
- disabled = false,
- active = false,
-
- -- Scrollbar states
- scrollbarHoveredVertical = false,
- scrollbarHoveredHorizontal = false,
- scrollbarDragging = false,
- hoveredScrollbar = nil,
- scrollbarDragOffset = 0,
- dragStartMouseX = 0,
- dragStartMouseY = 0,
- dragStartScrollX = 0,
- dragStartScrollY = 0,
-
- -- Scroll position
- scrollX = 0,
- scrollY = 0,
- _scrollX = 0,
- _scrollY = 0,
-
- -- Click tracking
- _clickCount = 0,
- _lastClickTime = nil,
- _lastClickButton = nil,
-
- -- Internal states
- _hovered = nil,
- _focused = nil,
- _cursorPosition = nil,
- _selectionStart = nil,
- _selectionEnd = nil,
- _textBuffer = "",
- _cursorBlinkTimer = 0,
- _cursorVisible = true,
- _cursorBlinkPaused = false,
- _cursorBlinkPauseTimer = 0,
-}
-
---- Check if a value equals the default for a key
----@param key string State key
----@param value any Value to check
----@return boolean isDefault True if value equals default
-local function isDefaultValue(key, value)
- local defaultVal = stateDefaults[key]
-
- -- If no default defined, check for common defaults
- if defaultVal == nil then
- -- Empty tables are default
- if type(value) == "table" and next(value) == nil then
- return true
- end
- -- nil values are default
- if value == nil then
- return true
- end
- -- Otherwise, not a default value
- return false
- end
-
- -- Compare values
- if type(value) == "table" then
- -- Empty tables are considered default
- if next(value) == nil then
- return true
- end
- -- For other tables, compare contents (shallow)
- if type(defaultVal) ~= "table" then
- return false
- end
- for k, v in pairs(value) do
- if defaultVal[k] ~= v then
- return false
- end
- end
- return true
- else
- return value == defaultVal
- end
-end
-
--- ====================
--- ID Generation
--- ====================
-
---- Generate a hash from a table of properties
----@param props table
----@param visited table|nil Tracking table to prevent circular references
----@param depth number|nil Current recursion depth
----@return string
-local function hashProps(props, visited, depth)
- if not props then
- return ""
- end
-
- -- Initialize visited table on first call
- visited = visited or {}
- depth = depth or 0
-
- -- Limit recursion depth to prevent deep nesting issues
- if depth > 3 then
- return "[deep]"
- end
-
- -- Check if we've already visited this table (circular reference)
- if visited[props] then
- return "[circular]"
- end
-
- -- Mark this table as visited
- visited[props] = true
-
- local parts = {}
- local keys = {}
-
- -- Properties to skip (they cause issues or aren't relevant for ID generation)
- local skipKeys = {
- onEvent = true,
- parent = true,
- children = true,
- onFocus = true,
- onBlur = true,
- onTextInput = true,
- onTextChange = true,
- onEnter = true,
- userdata = true,
- -- Dynamic input/state properties that should not affect ID stability
- text = true, -- Text content changes as user types
- placeholder = true, -- Placeholder text is presentational
- editable = true, -- Editable state can be toggled dynamically
- selectOnFocus = true, -- Input behavior flag
- autoGrow = true, -- Auto-grow behavior flag
- passwordMode = true, -- Password mode can be toggled
- }
-
- -- Collect and sort keys for consistent ordering
- for k in pairs(props) do
- if not skipKeys[k] then
- table.insert(keys, k)
- end
- end
- table.sort(keys)
-
- -- Build hash string from sorted key-value pairs
- for _, k in ipairs(keys) do
- local v = props[k]
- local vtype = type(v)
-
- if vtype == "string" or vtype == "number" or vtype == "boolean" then
- table.insert(parts, k .. "=" .. tostring(v))
- elseif vtype == "table" then
- table.insert(parts, k .. "={" .. hashProps(v, visited, depth + 1) .. "}")
- end
- end
-
- return table.concat(parts, ";")
-end
-
---- Generate a unique ID from call site and properties
----@param props table|nil Optional properties to include in ID generation
----@param parent table|nil Optional parent element for tree-based ID generation
----@return string
-function StateManager.generateID(props, parent)
- -- Get call stack information
- local info = debug.getinfo(3, "Sl") -- Level 3: caller of Element.new -> caller of generateID
-
- if not info then
- -- Fallback to random ID if debug info unavailable
- return "auto_" .. tostring(math.random(1000000, 9999999))
- end
-
- local source = info.source or "unknown"
- local line = info.currentline or 0
-
- -- Create base location key from source file and line number
- local filename = source:match("([^/\\]+)$") or source -- Get filename
- filename = filename:gsub("%.lua$", "") -- Remove .lua extension
- local locationKey = filename .. "_L" .. line
-
- -- If we have a parent, use tree-based ID generation for stability
- if parent and parent.id and parent.id ~= "" then
- -- For child elements, use call-site (file + line) like top-level elements
- -- This ensures the same call site always generates the same ID, even when
- -- retained children persist in parent.children array
- local baseID = parent.id .. "_" .. locationKey
-
- -- Count how many children have been created at THIS call site
- local callSiteKey = parent.id .. "_" .. locationKey
- callSiteCounters[callSiteKey] = (callSiteCounters[callSiteKey] or 0) + 1
- local instanceNum = callSiteCounters[callSiteKey]
-
- if instanceNum > 1 then
- baseID = baseID .. "_" .. instanceNum
- end
-
- -- Add property hash if provided (for additional differentiation)
- if props then
- local propHash = hashProps(props)
- if propHash ~= "" then
- -- Use first 8 chars of a simple hash
- local hash = 0
- for i = 1, #propHash do
- hash = (hash * 31 + string.byte(propHash, i)) % 1000000
- end
- baseID = baseID .. "_" .. hash
- end
- end
-
- return baseID
- end
-
- -- No parent (top-level element): use call-site counter approach
- -- Track how many elements have been created at this location
- callSiteCounters[locationKey] = (callSiteCounters[locationKey] or 0) + 1
- local instanceNum = callSiteCounters[locationKey]
-
- local baseID = locationKey
-
- -- Add instance number if multiple elements created at same location (e.g., in loops)
- if instanceNum > 1 then
- baseID = baseID .. "_" .. instanceNum
- end
-
- -- Add property hash if provided (for additional differentiation)
- if props then
- local propHash = hashProps(props)
- if propHash ~= "" then
- -- Use first 8 chars of a simple hash
- local hash = 0
- for i = 1, #propHash do
- hash = (hash * 31 + string.byte(propHash, i)) % 1000000
- end
- baseID = baseID .. "_" .. hash
- end
- end
-
- return baseID
-end
-
--- ====================
--- State Management
--- ====================
-
---- Initialize StateManager with dependencies
----@param deps table Dependencies: { ErrorHandler = ErrorHandler }
-function StateManager.init(deps)
- if type(deps) == "table" then
- ErrorHandler = deps.ErrorHandler
- end
-end
-
---- Get state for an element ID, creating if it doesn't exist
----@param id string Element ID
----@param defaultState table|nil Default state if creating new
----@return table state State table for the element
-function StateManager.getState(id, defaultState)
- if not id then
- ErrorHandler:error("StateManager", "SYS_001", {
- parameter = "id",
- value = "nil",
- })
- end
-
- -- Create state if it doesn't exist
- if not stateStore[id] then
- -- Start with empty state (sparse storage)
- stateStore[id] = defaultState or {}
-
- -- Create metadata
- stateMetadata[id] = {
- lastFrame = frameNumber,
- createdFrame = frameNumber,
- accessCount = 0,
- }
- else
- -- Update metadata
- local meta = stateMetadata[id]
- meta.lastFrame = frameNumber
- meta.accessCount = meta.accessCount + 1
- end
-
- return stateStore[id]
-end
-
---- Set state for an element ID (replaces entire state)
----@param id string Element ID
----@param state table State to store
-function StateManager.setState(id, state)
- if not id then
- ErrorHandler:error("StateManager", "SYS_001", {
- parameter = "id",
- value = "nil",
- })
- end
-
- -- Create sparse state (remove default values)
- local sparseState = {}
- for key, value in pairs(state) do
- if not isDefaultValue(key, value) then
- sparseState[key] = value
- end
- end
-
- stateStore[id] = sparseState
-
- -- Update or create metadata
- if not stateMetadata[id] then
- stateMetadata[id] = {
- lastFrame = frameNumber,
- createdFrame = frameNumber,
- accessCount = 1,
- }
- else
- stateMetadata[id].lastFrame = frameNumber
- end
-end
-
---- Update state for an element ID (merges with existing state)
----@param id string Element ID
----@param newState table New state values to merge
-function StateManager.updateState(id, newState)
- local state = StateManager.getState(id)
-
- -- Merge new state into existing state (with diffing optimization)
- local changed = false
- for key, value in pairs(newState) do
- if state[key] ~= value then
- state[key] = value
- changed = true
- end
- end
-
- -- Only update metadata if something actually changed
- if changed then
- stateMetadata[id].lastFrame = frameNumber
- end
-end
-
---- Update state only if values have changed (optimized for immediate mode)
----@param id string Element ID
----@param newState table New state values to merge
----@return boolean changed True if any values changed
-function StateManager.updateStateIfChanged(id, newState)
- local state = StateManager.getState(id)
- local changed = false
-
- for key, value in pairs(newState) do
- -- Skip if value hasn't changed (optimization)
- if state[key] ~= value then
- state[key] = value
- changed = true
- end
- end
-
- if changed then
- stateMetadata[id].lastFrame = frameNumber
- end
-
- return changed
-end
-
---- Clear state for a specific element ID
----@param id string Element ID
-function StateManager.clearState(id)
- stateStore[id] = nil
- stateMetadata[id] = nil
-end
-
---- Mark state as used this frame (updates last accessed frame)
----@param id string Element ID
-function StateManager.markStateUsed(id)
- if stateMetadata[id] then
- stateMetadata[id].lastFrame = frameNumber
- end
-end
-
--- ====================
--- Frame Management
--- ====================
-
---- Increment frame counter (called at frame start)
-function StateManager.incrementFrame()
- frameNumber = frameNumber + 1
- -- Reset call site counters for new frame
- callSiteCounters = {}
-end
-
---- Get current frame number
----@return number
-function StateManager.getFrameNumber()
- return frameNumber
-end
-
--- ====================
--- Granular State Access (Unified API for both modes)
--- ====================
-
---- Get a single state value by key for a given element ID.
---- Works identically in both modes — the caller does not need to know the mode.
----
---- Immediate mode: reads from persistent state store.
---- Retained mode: resolves through registered element field (cache-through).
----
----@param id string Element state ID
----@param key string State key
----@return any value The stored value, or nil if not found
-function StateManager.getStateValue(id, key)
- if not id or not key then
- ErrorHandler:error("StateManager", "SYS_001", {
- parameter = "id and key",
- value = "missing",
- })
- end
-
- -- Update metadata for access tracking
- if stateMetadata[id] then
- stateMetadata[id].lastFrame = frameNumber
- stateMetadata[id].accessCount = stateMetadata[id].accessCount + 1
- end
-
- if _immediateMode then
- -- Immediate mode: read from persistent state store
- local state = stateStore[id]
- if state then
- return state[key]
- end
- return nil
- else
- -- Retained mode: resolve through element field
- local element = statefulElements[id]
- if element then
- return element[key]
- end
- return nil
- end
-end
-
---- Set a single state value by key for a given element ID.
---- Works identically in both modes — the caller does not need to know the mode.
----
---- Immediate mode: marks dirty for flushFrame() persistence.
---- Retained mode: writes directly to element field (cache-through).
----
----@param id string Element state ID
----@param key string State key
----@param value any Value to store
-function StateManager.setStateValue(id, key, value)
- if not id or not key then
- ErrorHandler:error("StateManager", "SYS_001", {
- parameter = "id and key",
- value = "missing",
- })
- end
-
- -- Update metadata
- if not stateMetadata[id] then
- stateMetadata[id] = {
- lastFrame = frameNumber,
- createdFrame = frameNumber,
- accessCount = 1,
- }
- else
- stateMetadata[id].lastFrame = frameNumber
- end
-
- if _immediateMode then
- -- Immediate mode: mark dirty for flushFrame persistence
- local state = StateManager.getState(id)
- state[key] = value
- dirtyState[id] = dirtyState[id] or {}
- dirtyState[id][key] = true
- else
- -- Retained mode: write directly to element field
- local element = statefulElements[id]
- if element then
- element[key] = value
- end
- end
-end
-
--- ====================
--- Stateful Element Registration (Retained Mode Cache-Through)
--- ====================
-
---- Register an element instance for retained-mode cache-through.
---- After registration, getStateValue/setStateValue will resolve through the element's fields.
----
---- Called by Element in _construct phase.
----
----@param id string State ID (typically element.id)
----@param element table Element instance to link
-function StateManager.registerStateful(id, element)
- if not id or not element then
- return
- end
- statefulElements[id] = element
-end
-
---- Unregister an element instance.
---- After unregistration, retained-mode access will fall back to nil.
----
---- Called by Element in _cleanup phase.
----
----@param id string State ID to unregister
-function StateManager.unregisterStateful(id)
- if id then
- statefulElements[id] = nil
- end
-end
-
--- ====================
--- Frame Flush (Immediate Mode Dirty State Persistence)
--- ====================
-
---- Flush dirty state to persistent store at end of frame.
---- Called automatically at frame end in immediate mode.
---- Behaviors call setStateValue during update without knowing the mode.
----
---- In retained mode, this is a no-op (state is written directly to elements).
-function StateManager.flushFrame()
- if not _immediateMode then
- return
- end
-
- -- All dirty writes were already applied to stateStore during setStateValue
- -- This method exists for future extensions (e.g., batching, analytics)
- -- Reset dirty tracking for next frame
- dirtyState = {}
-end
-
--- ====================
--- Mode Configuration
--- ====================
-
---- Configure immediate mode state.
---- Called by Context when immediate mode is enabled/disabled.
----
----@param enabled boolean Whether immediate mode is active
-function StateManager.setImmediateMode(enabled)
- _immediateMode = enabled
-end
-
---- Check if immediate mode is active.
----@return boolean
-function StateManager.isImmediateMode()
- return _immediateMode
-end
-
---- Whether at-construction layout / eager initialization should run now.
---- Returns true in retained mode (layout eagerly), false in immediate mode
---- (layout is deferred to `FlexLove.endFrame` / FlexLove so it runs once all
---- elements for the frame have been created). This replaces the scattered
---- `if not _immediateMode then layoutChildren()` mode checks with a single
---- mode-aware query (behavior-mode-unification task 11).
----@return boolean
-function StateManager.shouldLayout()
- return not _immediateMode
-end
-
--- ====================
--- Cleanup & Maintenance
--- ====================
-
---- Clean up stale states (not accessed recently)
----@return number count Number of states cleaned up
-function StateManager.cleanup()
- local cleanedCount = 0
- local retentionFrames = config.stateRetentionFrames
-
- for id, meta in pairs(stateMetadata) do
- local framesSinceAccess = frameNumber - meta.lastFrame
-
- if framesSinceAccess > retentionFrames then
- stateStore[id] = nil
- stateMetadata[id] = nil
- cleanedCount = cleanedCount + 1
- end
- end
-
- -- Clean up empty states (sparse storage optimization)
- for id, state in pairs(stateStore) do
- if next(state) == nil then
- stateStore[id] = nil
- stateMetadata[id] = nil
- cleanedCount = cleanedCount + 1
- end
- end
-
- return cleanedCount
-end
-
---- Force cleanup if state count exceeds maximum
----@return number count Number of states cleaned up
-function StateManager.forceCleanupIfNeeded()
- local stateCount = StateManager.getStateCount()
-
- if stateCount > config.maxStateEntries then
- -- Clean up states not accessed in last 10 frames (aggressive)
- local cleanedCount = 0
-
- for id, meta in pairs(stateMetadata) do
- local framesSinceAccess = frameNumber - meta.lastFrame
-
- if framesSinceAccess > 10 then
- stateStore[id] = nil
- stateMetadata[id] = nil
- cleanedCount = cleanedCount + 1
- end
- end
-
- return cleanedCount
- end
-
- return 0
-end
-
---- Get total number of stored states
----@return number
-function StateManager.getStateCount()
- local count = 0
- for _ in pairs(stateStore) do
- count = count + 1
- end
- return count
-end
-
---- Clear all states
-function StateManager.clearAllStates()
- stateStore = {}
- stateMetadata = {}
-end
-
---- Configure state management
----@param newConfig {stateRetentionFrames?: number, maxStateEntries?: number}
-function StateManager.configure(newConfig)
- if newConfig.stateRetentionFrames then
- config.stateRetentionFrames = newConfig.stateRetentionFrames
- end
- if newConfig.maxStateEntries then
- config.maxStateEntries = newConfig.maxStateEntries
- end
-end
-
---- Get state statistics for debugging
----@return table stats State usage statistics
-function StateManager.getStats()
- local stateCount = StateManager.getStateCount()
- local oldest = nil
- local newest = nil
-
- for _, meta in pairs(stateMetadata) do
- if not oldest or meta.createdFrame < oldest then
- oldest = meta.createdFrame
- end
- if not newest or meta.createdFrame > newest then
- newest = meta.createdFrame
- end
- end
-
- -- Count callSiteCounters
- local callSiteCount = 0
- for _ in pairs(callSiteCounters) do
- callSiteCount = callSiteCount + 1
- end
-
- -- Warn if callSiteCounters is unexpectedly large
- if callSiteCount > 1000 then
- if ErrorHandler then
- ErrorHandler.warn("StateManager", "STATE_001", {
- count = callSiteCount,
- expected = "near 0",
- frameNumber = frameNumber,
- })
- end
- end
-
- return {
- stateCount = stateCount,
- frameNumber = frameNumber,
- oldestState = oldest,
- newestState = newest,
- callSiteCounterCount = callSiteCount,
- }
-end
-
---- Get internal state (for debugging/profiling only)
----@return table internal {stateStore, stateMetadata, callSiteCounters}
-function StateManager._getInternalState()
- return {
- stateStore = stateStore,
- stateMetadata = stateMetadata,
- callSiteCounters = callSiteCounters,
- }
-end
-
---- Reset the entire state system (for testing)
-function StateManager.reset()
- stateStore = {}
- stateMetadata = {}
- frameNumber = 0
- callSiteCounters = {}
- statefulElements = {}
- dirtyState = {}
- _immediateMode = false
-end
-
--- ====================
--- Convenience Functions (for backward compatibility)
--- ====================
-
---- Check if an element is currently hovered
----@param id string Element ID
----@return boolean
-function StateManager.isHovered(id)
- local state = StateManager.getState(id)
- return state.hover or false
-end
-
---- Check if an element is currently pressed
----@param id string Element ID
----@return boolean
-function StateManager.isPressed(id)
- local state = StateManager.getState(id)
- return state.pressed or false
-end
-
---- Check if an element is currently focused
----@param id string Element ID
----@return boolean
-function StateManager.isFocused(id)
- local state = StateManager.getState(id)
- return state.focused or false
-end
-
---- Check if an element is disabled
----@param id string Element ID
----@return boolean
-function StateManager.isDisabled(id)
- local state = StateManager.getState(id)
- return state.disabled or false
-end
-
---- Check if an element is active (e.g., input focused)
----@param id string Element ID
----@return boolean
-function StateManager.isActive(id)
- local state = StateManager.getState(id)
- return state.active or false
-end
-
-return StateManager
diff --git a/libs/flexlove/modules/TextEditor.lua b/libs/flexlove/modules/TextEditor.lua
deleted file mode 100644
index 35af6354..00000000
--- a/libs/flexlove/modules/TextEditor.lua
+++ /dev/null
@@ -1,1783 +0,0 @@
-local UTF8 = require((...):match("(.-)[^%.]+$") .. "UTF8")
-local utf8 = UTF8
-
----@class TextEditor
----@field editable boolean
----@field multiline boolean
----@field passwordMode boolean
----@field textWrap boolean|"word"|"char"
----@field maxLines number?
----@field maxLength number?
----@field placeholder string?
----@field inputType "text"|"number"|"email"|"url"
----@field textOverflow "clip"|"ellipsis"|"scroll"
----@field scrollable boolean
----@field autoGrow boolean
----@field selectOnFocus boolean
----@field sanitize boolean
----@field allowNewlines boolean
----@field allowTabs boolean
----@field customSanitizer function?
----@field cursorColor Color?
----@field selectionColor Color?
----@field cursorBlinkRate number
----@field _textBuffer string
----@field _lines table?
----@field _wrappedLines table?
----@field _textDirty boolean
----@field _cursorPosition number
----@field _cursorLine number
----@field _cursorColumn number
----@field _cursorBlinkTimer number
----@field _cursorVisible boolean
----@field _cursorBlinkPaused boolean
----@field _cursorBlinkPauseTimer number
----@field _selectionStart number?
----@field _selectionEnd number?
----@field _selectionAnchor number?
----@field _focused boolean
----@field _textScrollX number
----@field onFocus fun(element:Element)?
----@field onBlur fun(element:Element)?
----@field onTextInput fun(element:Element, text:string)?
----@field onTextChange fun(element:Element, text:string)?
----@field onEnter fun(element:Element)?
----@field onSanitize fun(element:Element, original:string, sanitized:string)?
----@field _Context table
----@field _StateManager table
----@field _Color table
----@field _FONT_CACHE table
----@field _getModifiers function
----@field _utils table
----@field _textDragOccurred boolean?
-local TextEditor = {}
-TextEditor.__index = TextEditor
-
----@class TextEditorConfig
----@field editable boolean -- Whether text is editable
----@field multiline boolean -- Whether multi-line is supported
----@field passwordMode boolean -- Whether to mask text
----@field textWrap boolean|"word"|"char" -- Text wrapping mode
----@field maxLines number? -- Maximum number of lines
----@field maxLength number? -- Maximum text length in characters
----@field placeholder string? -- Placeholder text when empty
----@field inputType "text"|"number"|"email"|"url" -- Input validation type
----@field textOverflow "clip"|"ellipsis"|"scroll" -- Text overflow behavior
----@field scrollable boolean -- Whether text is scrollable
----@field autoGrow boolean -- Whether element auto-grows with text
----@field selectOnFocus boolean -- Whether to select all text on focus
----@field sanitize boolean? -- Whether to sanitize text input (default: true)
----@field allowNewlines boolean? -- Whether to allow newline characters (default: true in multiline)
----@field allowTabs boolean? -- Whether to allow tab characters (default: true)
----@field customSanitizer function? -- Custom sanitization function
----@field cursorColor Color? -- Cursor color
----@field selectionColor Color? -- Selection background color
----@field cursorBlinkRate number -- Cursor blink rate in seconds
-
----Create a new TextEditor instance
----@param config TextEditorConfig
----@param deps table Dependencies {Context, StateManager, Color, utils}
----@return table TextEditor instance
-function TextEditor.new(config, deps)
- local self = setmetatable({}, TextEditor)
-
- -- Store dependencies
- self._Context = deps.Context
- self._StateManager = deps.StateManager
- self._Color = deps.Color
- self._FONT_CACHE = deps.utils.FONT_CACHE
- self._getModifiers = deps.utils.getModifiers
- self._utils = deps.utils
-
- -- Store configuration
- self.editable = config.editable or false
- self.multiline = config.multiline or false
- self.passwordMode = config.passwordMode or false
- self.textWrap = config.textWrap
- self.maxLines = config.maxLines
- self.maxLength = config.maxLength
- self.placeholder = config.placeholder
- self.inputType = config.inputType or "text"
- self.textOverflow = config.textOverflow or "clip"
- self.scrollable = config.scrollable
- self.autoGrow = config.autoGrow
- self.selectOnFocus = config.selectOnFocus or false
- self.cursorColor = config.cursorColor
- self.selectionColor = config.selectionColor
- self.cursorBlinkRate = config.cursorBlinkRate or 0.5
-
- -- Sanitization configuration
- self.sanitize = config.sanitize ~= false -- Default to true
- -- If allowNewlines is explicitly set, use that value; otherwise follow multiline setting
- if config.allowNewlines ~= nil then
- self.allowNewlines = config.allowNewlines
- else
- self.allowNewlines = self.multiline
- end
- self.allowTabs = config.allowTabs ~= false -- Default to true
- self.customSanitizer = config.customSanitizer
-
- -- Initialize text buffer state (with sanitization)
- local initialText = config.text or ""
- self._textBuffer = self:_sanitizeText(initialText)
- self._lines = nil
- self._wrappedLines = nil
- self._textDirty = true
-
- -- Initialize cursor state
- self._cursorPosition = 0
- self._cursorLine = 1
- self._cursorColumn = 0
- self._cursorBlinkTimer = 0
- self._cursorVisible = true
- self._cursorBlinkPaused = false
- self._cursorBlinkPauseTimer = 0
-
- -- Initialize selection state
- self._selectionStart = nil
- self._selectionEnd = nil
- self._selectionAnchor = nil
-
- -- Initialize focus state
- self._focused = false
-
- -- Initialize scroll state
- self._textScrollX = 0
-
- -- Store callbacks
- self.onFocus = config.onFocus
- self.onBlur = config.onBlur
- self.onTextInput = config.onTextInput
- self.onTextChange = config.onTextChange
- self.onEnter = config.onEnter
- self.onSanitize = config.onSanitize
-
- return self
-end
-
----Internal: Sanitize text input
----@param text string -- Text to sanitize
----@return string -- Sanitized text
-function TextEditor:_sanitizeText(text)
- if not self.sanitize then
- return text
- end
-
- -- Use custom sanitizer if provided
- if self.customSanitizer then
- return self.customSanitizer(text) or text
- end
-
- local options = {
- maxLength = self.maxLength,
- allowNewlines = self.allowNewlines,
- allowTabs = self.allowTabs,
- trimWhitespace = false, -- Preserve whitespace in text editors
- }
-
- local sanitized = self._utils.sanitizeText(text, options)
-
- return sanitized
-end
-
----Restore state from StateManager (for immediate mode)
----@param element table The parent Element instance
-function TextEditor:restoreState(element)
- -- Restore state from StateManager. Mode-aware via Context.isImmediateMode:
- -- in retained mode the TextEditor persists between frames so nothing to
- -- restore (behavior-mode-unification task 11).
- if element._stateId and self._Context.isImmediateMode() then
- local state = self._StateManager.getState(element._stateId)
- if state then
- if state._focused then
- self._focused = true
- self._Context.setFocused(element)
- end
- if state._textBuffer and state._textBuffer ~= "" then
- self._textBuffer = state._textBuffer
- end
- if state._cursorPosition then
- self._cursorPosition = state._cursorPosition
- end
- if state._selectionStart then
- self._selectionStart = state._selectionStart
- end
- if state._selectionEnd then
- self._selectionEnd = state._selectionEnd
- end
- if state._cursorBlinkTimer then
- self._cursorBlinkTimer = state._cursorBlinkTimer
- end
- if state._cursorVisible ~= nil then
- self._cursorVisible = state._cursorVisible
- end
- if state._cursorBlinkPaused ~= nil then
- self._cursorBlinkPaused = state._cursorBlinkPaused
- end
- if state._cursorBlinkPauseTimer then
- self._cursorBlinkPauseTimer = state._cursorBlinkPauseTimer
- end
- end
- end
-end
-
--- ====================
--- Text Buffer Management
--- ====================
-
----Get current text buffer
----@return string
-function TextEditor:getText()
- return self._textBuffer or ""
-end
-
----Set text buffer and mark dirty
----@param element Element? The parent element (for state saving)
----@param text string
----@param skipSanitization boolean? -- Skip sanitization (for trusted input)
-function TextEditor:setText(element, text, skipSanitization)
- text = text or ""
-
- -- Sanitize text unless explicitly skipped
- if not skipSanitization then
- local originalText = text
- text = self:_sanitizeText(text)
-
- -- Trigger onSanitize callback if text was sanitized
- if text ~= originalText and self.onSanitize and element then
- self.onSanitize(element, originalText, text)
- end
- end
-
- self._textBuffer = text
- self:_markTextDirty()
- self:_updateTextIfDirty(element)
- self:_validateCursorPosition()
- self:_saveState(element)
-end
-
----Insert text at position
----@param element Element The parent element (for state saving)
----@param text string -- Text to insert
----@param position number? -- Position to insert at (default: cursor position)
----@param skipSanitization boolean? -- Skip sanitization (for internal use)
-function TextEditor:insertText(element, text, position, skipSanitization)
- position = position or self._cursorPosition
- local buffer = self._textBuffer or ""
-
- -- Sanitize text unless explicitly skipped
- if not skipSanitization then
- text = self:_sanitizeText(text)
- end
-
- -- Check if text is empty after sanitization
- if not text or text == "" then
- return
- end
-
- -- Check maxLength constraint before inserting
- if self.maxLength then
- local currentLength = utf8.len(buffer) or 0
- local textLength = utf8.len(text) or 0
- local newLength = currentLength + textLength
-
- if newLength > self.maxLength then
- -- Truncate text to fit
- local remaining = self.maxLength - currentLength
- if remaining <= 0 then
- return
- end
- -- Truncate to remaining characters
- local truncated = ""
- local count = 0
- for _, code in utf8.codes(text) do
- if count >= remaining then
- break
- end
- truncated = truncated .. utf8.char(code)
- count = count + 1
- end
- text = truncated
- end
- end
-
- -- Convert character position to byte offset
- local byteOffset = utf8.offset(buffer, position + 1) or (#buffer + 1)
-
- -- Insert text
- local before = buffer:sub(1, byteOffset - 1)
- local after = buffer:sub(byteOffset)
- self._textBuffer = before .. text .. after
-
- self._cursorPosition = position + utf8.len(text)
-
- self:_markTextDirty()
- self:_updateTextIfDirty(element)
- self:_validateCursorPosition()
- self:_resetCursorBlink(element, true)
- self:_saveState(element)
-end
-
----Delete text in range
----@param element Element The parent element (for state saving)
----@param startPos number -- Start position (inclusive)
----@param endPos number -- End position (inclusive)
-function TextEditor:deleteText(element, startPos, endPos)
- local buffer = self._textBuffer or ""
-
- -- Ensure valid range
- local textLength = utf8.len(buffer)
- startPos = math.max(0, math.min(startPos, textLength))
- endPos = math.max(0, math.min(endPos, textLength))
-
- if startPos > endPos then
- startPos, endPos = endPos, startPos
- end
-
- -- Convert character positions to byte offsets
- local startByte = utf8.offset(buffer, startPos + 1) or 1
- local endByte = utf8.offset(buffer, endPos + 1) or (#buffer + 1)
-
- -- Delete text
- local before = buffer:sub(1, startByte - 1)
- local after = buffer:sub(endByte)
- self._textBuffer = before .. after
-
- self:_markTextDirty()
- self:_updateTextIfDirty(element)
- self:_resetCursorBlink(element, true)
- self:_saveState(element)
-end
-
----Replace text in range
----@param element Element The parent element (for state saving)
----@param startPos number -- Start position (inclusive)
----@param endPos number -- End position (inclusive)
----@param newText string -- Replacement text
-function TextEditor:replaceText(element, startPos, endPos, newText)
- self:deleteText(element, startPos, endPos)
- self:insertText(element, newText, startPos)
-end
-
----Mark text as dirty (needs recalculation)
-function TextEditor:_markTextDirty()
- self._textDirty = true
-end
-
----Update text if dirty (recalculate lines and wrapping)
----@param element Element? The parent element (for wrapping calculations)
-function TextEditor:_updateTextIfDirty(element)
- if not self._textDirty then
- return
- end
-
- self:_splitLines()
- self:_calculateWrapping(element)
- self:_validateCursorPosition()
- self._textDirty = false
-end
-
--- ====================
--- Line Splitting and Wrapping
--- ====================
-
----Split text into lines (for multi-line text)
-function TextEditor:_splitLines()
- if not self.multiline then
- self._lines = { self._textBuffer or "" }
- return
- end
-
- self._lines = {}
- local text = self._textBuffer or ""
-
- -- Split on newlines
- for line in (text .. "\n"):gmatch("([^\n]*)\n") do
- table.insert(self._lines, line)
- end
-
- -- Ensure at least one line
- if #self._lines == 0 then
- self._lines = { "" }
- end
-end
-
----Calculate text wrapping
----@param element Element? The parent element
-function TextEditor:_calculateWrapping(element)
- if not self.textWrap or not element then
- self._wrappedLines = nil
- return
- end
-
- self._wrappedLines = {}
- local availableWidth = element.width - element.padding.left - element.padding.right
-
- for lineNum, line in ipairs(self._lines or {}) do
- if line == "" then
- table.insert(self._wrappedLines, {
- text = "",
- startIdx = 0,
- endIdx = 0,
- lineNum = lineNum,
- })
- else
- local wrappedParts = self:_wrapLine(element, line, availableWidth)
- for _, part in ipairs(wrappedParts) do
- part.lineNum = lineNum
- table.insert(self._wrappedLines, part)
- end
- end
- end
-end
-
----Wrap a single line of text
----@param element Element The parent element
----@param line string -- Line to wrap
----@param maxWidth number -- Maximum width in pixels
----@return table -- Array of wrapped line parts
-function TextEditor:_wrapLine(element, line, maxWidth)
- if not element then
- return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
- end
-
- -- Delegate to Renderer
- return element._renderer:wrapLine(element, line, maxWidth)
-end
-
--- ====================
--- Cursor Management
--- ====================
-
----Set cursor position
----@param element Element? The parent element (for scroll updates)
----@param position number -- Character index (0-based)
-function TextEditor:setCursorPosition(element, position)
- self._cursorPosition = position
- self:_validateCursorPosition()
- self:_resetCursorBlink(element)
-end
-
----Get cursor position
----@return number -- Character index (0-based)
-function TextEditor:getCursorPosition()
- return self._cursorPosition
-end
-
----Move cursor by delta characters
----@param element Element? The parent element (for scroll updates)
----@param delta number -- Number of characters to move (positive or negative)
-function TextEditor:moveCursorBy(element, delta)
- self._cursorPosition = self._cursorPosition + delta
- self:_validateCursorPosition()
- self:_resetCursorBlink(element)
-end
-
----Move cursor to start of text
----@param element Element? The parent element (for scroll updates)
-function TextEditor:moveCursorToStart(element)
- self._cursorPosition = 0
- self:_resetCursorBlink(element)
-end
-
----Move cursor to end of text
----@param element Element? The parent element (for scroll updates)
-function TextEditor:moveCursorToEnd(element)
- local textLength = utf8.len(self._textBuffer or "")
- self._cursorPosition = textLength
- self:_resetCursorBlink(element)
-end
-
----Move cursor to start of current line
----@param element Element? The parent element (for scroll updates)
-function TextEditor:moveCursorToLineStart(element)
- -- For now, just move to start (will be enhanced for multi-line)
- self:moveCursorToStart(element)
-end
-
----Move cursor to end of current line
----@param element Element? The parent element (for scroll updates)
-function TextEditor:moveCursorToLineEnd(element)
- -- For now, just move to end (will be enhanced for multi-line)
- self:moveCursorToEnd(element)
-end
-
----Move cursor to start of previous word
-function TextEditor:moveCursorToPreviousWord()
- if not self._textBuffer then
- return
- end
-
- local text = self._textBuffer
- local pos = self._cursorPosition
-
- if pos <= 0 then
- return
- end
-
- -- Helper function to get character at position
- local function getCharAt(p)
- if p < 0 or p >= utf8.len(text) then
- return nil
- end
- local offset1 = utf8.offset(text, p + 1)
- local offset2 = utf8.offset(text, p + 2)
- if not offset1 then
- return nil
- end
- if not offset2 then
- return text:sub(offset1)
- end
- return text:sub(offset1, offset2 - 1)
- end
-
- -- Skip any whitespace/punctuation before current position
- while pos > 0 do
- local char = getCharAt(pos - 1)
- if char and char:match("[%w]") then
- break
- end
- pos = pos - 1
- end
-
- -- Move to start of current word
- while pos > 0 do
- local char = getCharAt(pos - 1)
- if not char or not char:match("[%w]") then
- break
- end
- pos = pos - 1
- end
-
- self._cursorPosition = pos
- self:_validateCursorPosition()
-end
-
----Move cursor to start of next word
-function TextEditor:moveCursorToNextWord()
- if not self._textBuffer then
- return
- end
-
- local text = self._textBuffer
- local textLength = utf8.len(text) or 0
- local pos = self._cursorPosition
-
- if pos >= textLength then
- return
- end
-
- -- Helper function to get character at position
- local function getCharAt(p)
- if p < 0 or p >= textLength then
- return nil
- end
- local offset1 = utf8.offset(text, p + 1)
- local offset2 = utf8.offset(text, p + 2)
- if not offset1 then
- return nil
- end
- if not offset2 then
- return text:sub(offset1)
- end
- return text:sub(offset1, offset2 - 1)
- end
-
- -- Skip current word
- while pos < textLength do
- local char = getCharAt(pos)
- if not char or not char:match("[%w]") then
- break
- end
- pos = pos + 1
- end
-
- -- Skip any whitespace/punctuation
- while pos < textLength do
- local char = getCharAt(pos)
- if char and char:match("[%w]") then
- break
- end
- pos = pos + 1
- end
-
- self._cursorPosition = pos
- self:_validateCursorPosition()
-end
-
----Validate cursor position (ensure it's within text bounds)
-function TextEditor:_validateCursorPosition()
- local textLength = utf8.len(self._textBuffer or "") or 0
- local cursorPos = tonumber(self._cursorPosition) or 0
- self._cursorPosition = math.max(0, math.min(cursorPos, textLength))
-end
-
----Reset cursor blink (show cursor immediately)
----@param element Element? The parent element (for scroll updates)
----@param pauseBlink boolean|nil -- Whether to pause blinking (for typing)
-function TextEditor:_resetCursorBlink(element, pauseBlink)
- self._cursorBlinkTimer = 0
- self._cursorVisible = true
-
- if pauseBlink then
- self._cursorBlinkPaused = true
- self._cursorBlinkPauseTimer = 0
- end
-
- self:_updateTextScroll(element)
-end
-
----Update text scroll offset to keep cursor visible
----@param element Element? The parent element
-function TextEditor:_updateTextScroll(element)
- if not element or self.multiline then
- return
- end
-
- local font = self:_getFont(element)
- if not font then
- return
- end
-
- -- Calculate cursor X position in text coordinates
- local cursorText = ""
- if self._textBuffer and self._textBuffer ~= "" and self._cursorPosition > 0 then
- local byteOffset = utf8.offset(self._textBuffer, self._cursorPosition + 1)
- if byteOffset then
- cursorText = self._textBuffer:sub(1, byteOffset - 1)
- end
- end
- local cursorX = font:getWidth(cursorText)
-
- -- Get available text area width
- local textAreaWidth = element.width
- local scaledContentPadding = element:getScaledContentPadding()
- if scaledContentPadding then
- local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
- end
-
- -- Add some padding on the right for the cursor
- local cursorPadding = 4
- local visibleWidth = textAreaWidth - cursorPadding
-
- -- Adjust scroll to keep cursor visible
- if cursorX - self._textScrollX < 0 then
- self._textScrollX = cursorX
- elseif cursorX - self._textScrollX > visibleWidth then
- self._textScrollX = cursorX - visibleWidth
- end
-
- -- Ensure we don't scroll past the beginning
- self._textScrollX = math.max(0, self._textScrollX)
-end
-
----Get cursor screen position for rendering (handles multiline text)
----@param element Element? The parent element
----@return number, number -- Cursor X and Y position relative to content area
-function TextEditor:_getCursorScreenPosition(element)
- local font = self:_getFont(element)
- if not font then
- return 0, 0
- end
-
- local text = self._textBuffer or ""
- local cursorPos = self._cursorPosition or 0
-
- -- Apply password masking for cursor position calculation
- local textForMeasurement = text
- if self.passwordMode and text ~= "" then
- textForMeasurement = string.rep("•", utf8.len(text))
- end
-
- -- For single-line text, calculate simple X position
- if not self.multiline then
- local cursorText = ""
- if textForMeasurement ~= "" and cursorPos > 0 then
- local byteOffset = utf8.offset(textForMeasurement, cursorPos + 1)
- if byteOffset then
- cursorText = textForMeasurement:sub(1, byteOffset - 1)
- end
- end
- return font:getWidth(cursorText), 0
- end
-
- -- For multiline text, we need to find which wrapped line the cursor is on
- self:_updateTextIfDirty(element)
-
- if not element then
- return 0, 0
- end
-
- -- Get text area width for wrapping
- local textAreaWidth = element.width
- local scaledContentPadding = element:getScaledContentPadding()
- if scaledContentPadding then
- local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
- end
-
- -- Split text by actual newlines first
- local lines = {}
- for line in (text .. "\n"):gmatch("([^\n]*)\n") do
- table.insert(lines, line)
- end
- if #lines == 0 then
- lines = { "" }
- end
-
- -- Track character position as we iterate through lines
- local charCount = 0
- local cursorX = 0
- local cursorY = 0
- local lineHeight = font:getHeight()
-
- for lineNum, line in ipairs(lines) do
- local lineLength = utf8.len(line) or 0
-
- -- Check if cursor is on this line
- if cursorPos <= charCount + lineLength then
- local posInLine = cursorPos - charCount
-
- -- If text wrapping is enabled, find which wrapped segment
- if self.textWrap and textAreaWidth > 0 then
- local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)
-
- for segmentIdx, segment in ipairs(wrappedSegments) do
- if posInLine >= segment.startIdx and posInLine <= segment.endIdx then
- local posInSegment = posInLine - segment.startIdx
- local segmentText = ""
- if posInSegment > 0 and segment.text ~= "" then
- local endByte = utf8.offset(segment.text, posInSegment + 1)
- if endByte then
- segmentText = segment.text:sub(1, endByte - 1)
- else
- segmentText = segment.text
- end
- end
- cursorX = font:getWidth(segmentText)
- cursorY = (lineNum - 1) * lineHeight + (segmentIdx - 1) * lineHeight
-
- return cursorX, cursorY
- end
- end
- else
- -- No wrapping, simple calculation
- local lineText = ""
- if posInLine > 0 then
- local endByte = utf8.offset(line, posInLine + 1)
- if endByte then
- lineText = line:sub(1, endByte - 1)
- else
- lineText = line
- end
- end
- cursorX = font:getWidth(lineText)
- cursorY = (lineNum - 1) * lineHeight
- return cursorX, cursorY
- end
- end
-
- charCount = charCount + lineLength + 1
- end
-
- -- Cursor is at the very end
- return 0, #lines * lineHeight
-end
-
--- ====================
--- Selection Management
--- ====================
-
----Set selection range
----@param element Element? The parent element (for scroll updates)
----@param startPos number -- Start position (inclusive)
----@param endPos number -- End position (inclusive)
-function TextEditor:setSelection(element, startPos, endPos)
- local textLength = utf8.len(self._textBuffer or "")
- self._selectionStart = math.max(0, math.min(startPos, textLength))
- self._selectionEnd = math.max(0, math.min(endPos, textLength))
-
- -- Ensure start <= end
- if self._selectionStart > self._selectionEnd then
- self._selectionStart, self._selectionEnd = self._selectionEnd, self._selectionStart
- end
-
- self:_resetCursorBlink(element)
-end
-
----Get selection range
----@return number?, number? -- Start and end positions, or nil if no selection
-function TextEditor:getSelection()
- if not self:hasSelection() then
- return nil, nil
- end
- return self._selectionStart, self._selectionEnd
-end
-
----Check if there is an active selection
----@return boolean
-function TextEditor:hasSelection()
- return self._selectionStart ~= nil and self._selectionEnd ~= nil and self._selectionStart ~= self._selectionEnd
-end
-
----Clear selection
-function TextEditor:clearSelection()
- self._selectionStart = nil
- self._selectionEnd = nil
- self._selectionAnchor = nil
-end
-
----Select all text
----@param element Element? The parent element (for scroll updates)
-function TextEditor:selectAll(element)
- local textLength = utf8.len(self._textBuffer or "")
- self._selectionStart = 0
- self._selectionEnd = textLength
- self:_resetCursorBlink(element)
-end
-
----Get selected text
----@return string? -- Selected text or nil if no selection
-function TextEditor:getSelectedText()
- if not self:hasSelection() then
- return nil
- end
-
- local startPos, endPos = self:getSelection()
- if not startPos or not endPos then
- return nil
- end
-
- -- Convert character indices to byte offsets
- local text = self._textBuffer or ""
- local startByte = utf8.offset(text, startPos + 1)
- local endByte = utf8.offset(text, endPos + 1)
-
- if not startByte then
- return ""
- end
-
- if endByte then
- endByte = endByte - 1
- end
-
- return string.sub(text, startByte, endByte)
-end
-
----Delete selected text
----@param element Element The parent element (for state saving)
----@return boolean -- True if text was deleted
-function TextEditor:deleteSelection(element)
- if not self:hasSelection() then
- return false
- end
-
- local startPos, endPos = self:getSelection()
- if not startPos or not endPos then
- return false
- end
-
- self:deleteText(element, startPos, endPos)
- self:clearSelection()
- self._cursorPosition = startPos
- self:_validateCursorPosition()
- self:_saveState(element)
-
- -- Sync display text and auto-grow height on the owning element
- if element then
- element.text = self:getText()
- self:updateAutoGrowHeight(element)
- end
-
- return true
-end
-
----Get selection rectangles for rendering
----@param element Element The parent element
----@param selStart number -- Selection start position
----@param selEnd number -- Selection end position
----@return table -- Array of rectangles {x, y, width, height}
-function TextEditor:_getSelectionRects(element, selStart, selEnd)
- local font = self:_getFont(element)
- if not font or not element then
- return {}
- end
-
- local text = self._textBuffer or ""
- local rects = {}
-
- -- Apply password masking
- local textForMeasurement = text
- if self.passwordMode and text ~= "" then
- textForMeasurement = string.rep("•", utf8.len(text))
- end
-
- -- For single-line text, calculate simple rectangle
- if not self.multiline then
- local startByte = utf8.offset(textForMeasurement, selStart + 1)
- local endByte = utf8.offset(textForMeasurement, selEnd + 1)
-
- if startByte and endByte then
- local beforeSelection = textForMeasurement:sub(1, startByte - 1)
- local selectedText = textForMeasurement:sub(startByte, endByte - 1)
- local selX = font:getWidth(beforeSelection)
- local selWidth = font:getWidth(selectedText)
- local selY = 0
- local selHeight = font:getHeight()
-
- table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
- end
-
- return rects
- end
-
- -- For multiline text, handle line wrapping
- self:_updateTextIfDirty(element)
-
- -- Get text area width for wrapping
- local textAreaWidth = element.width
- local scaledContentPadding = element:getScaledContentPadding()
- if scaledContentPadding then
- local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
- end
-
- -- Split text by actual newlines
- local lines = {}
- for line in (text .. "\n"):gmatch("([^\n]*)\n") do
- table.insert(lines, line)
- end
- if #lines == 0 then
- lines = { "" }
- end
-
- local lineHeight = font:getHeight()
- local charCount = 0
- local visualLineNum = 0
-
- for lineNum, line in ipairs(lines) do
- local lineLength = utf8.len(line) or 0
- local lineStartChar = charCount
- local lineEndChar = charCount + lineLength
-
- if selEnd > lineStartChar and selStart <= lineEndChar then
- local selStartInLine = math.max(0, selStart - charCount)
- local selEndInLine = math.min(lineLength, selEnd - charCount)
-
- if self.textWrap and textAreaWidth > 0 then
- local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)
-
- for segmentIdx, segment in ipairs(wrappedSegments) do
- if selEndInLine > segment.startIdx and selStartInLine <= segment.endIdx then
- local segSelStart = math.max(segment.startIdx, selStartInLine)
- local segSelEnd = math.min(segment.endIdx, selEndInLine)
-
- local beforeText = ""
- local selectedText = ""
-
- if segSelStart > segment.startIdx then
- local startByte = utf8.offset(segment.text, segSelStart - segment.startIdx + 1)
- if startByte then
- beforeText = segment.text:sub(1, startByte - 1)
- end
- end
-
- local selStartByte = utf8.offset(segment.text, segSelStart - segment.startIdx + 1)
- local selEndByte = utf8.offset(segment.text, segSelEnd - segment.startIdx + 1)
- if selStartByte and selEndByte then
- selectedText = segment.text:sub(selStartByte, selEndByte - 1)
- end
-
- local selX = font:getWidth(beforeText)
- local selWidth = font:getWidth(selectedText)
- local selY = visualLineNum * lineHeight
- local selHeight = lineHeight
-
- table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
- end
-
- visualLineNum = visualLineNum + 1
- end
- else
- -- No wrapping
- local beforeText = ""
- local selectedText = ""
-
- if selStartInLine > 0 then
- local startByte = utf8.offset(line, selStartInLine + 1)
- if startByte then
- beforeText = line:sub(1, startByte - 1)
- end
- end
-
- local selStartByte = utf8.offset(line, selStartInLine + 1)
- local selEndByte = utf8.offset(line, selEndInLine + 1)
- if selStartByte and selEndByte then
- selectedText = line:sub(selStartByte, selEndByte - 1)
- end
-
- local selX = font:getWidth(beforeText)
- local selWidth = font:getWidth(selectedText)
- local selY = visualLineNum * lineHeight
- local selHeight = lineHeight
-
- table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
- visualLineNum = visualLineNum + 1
- end
- else
- -- Selection doesn't intersect, but count visual lines
- if self.textWrap and textAreaWidth > 0 then
- local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)
- visualLineNum = visualLineNum + #wrappedSegments
- else
- visualLineNum = visualLineNum + 1
- end
- end
-
- charCount = charCount + lineLength + 1
- end
-
- return rects
-end
-
--- ====================
--- Focus Management
--- ====================
-
----Focus this element for keyboard input
----@param element Element The parent element
-function TextEditor:focus(element)
- if not element then
- return
- end
-
- -- Use centralized Context focus management
- self._Context.setFocused(element)
- self._focused = true
-
- self:_resetCursorBlink(element)
-
- if self.selectOnFocus then
- self:selectAll(element)
- else
- self:moveCursorToEnd(element)
- end
-
- if self.onFocus then
- self.onFocus(element)
- end
-
- self:_saveState(element)
-end
-
----Remove focus from this element
----@param element Element The parent element
-function TextEditor:blur(element)
- if not element then
- return
- end
-
- self._focused = false
-
- -- Clear focused element in Context if this element is currently focused
- -- Use direct assignment to avoid circular call back to blur()
- if self._Context.getFocused() == element then
- self._Context._focusedElement = nil
- end
-
- if self.onBlur then
- self.onBlur(element)
- end
-
- self:_saveState(element)
-end
-
----Check if this element is focused
----@return boolean
-function TextEditor:isFocused()
- return self._focused == true
-end
-
--- ====================
--- Input Handling
--- ====================
-
----Handle text input (character insertion)
----@param element Element The parent element
----@param text string
-function TextEditor:handleTextInput(element, text)
- if not self._focused then
- return
- end
-
- -- Trigger onTextInput callback if defined
- if self.onTextInput then
- local result = self.onTextInput(element, text)
- if result == false then
- return
- end
- end
-
- local oldText = self._textBuffer
-
- -- Delete selection if exists
- if self:hasSelection() then
- self:deleteSelection(element)
- end
-
- -- Insert text at cursor position
- self:insertText(element, text)
- -- Trigger onTextChange callback
- if self.onTextChange and self._textBuffer ~= oldText then
- self.onTextChange(element, self._textBuffer, oldText)
- end
-
- self:_saveState(element)
-end
-
----Handle key press (special keys)
----@param element Element The parent element
----@param key string -- Key name
----@param scancode string -- Scancode
----@param isrepeat boolean -- Whether this is a key repeat
-function TextEditor:handleKeyPress(element, key, scancode, isrepeat)
- if not self._focused then
- return
- end
-
- local modifiers = self._getModifiers()
- local ctrl = modifiers.ctrl or modifiers.super
-
- -- Handle cursor movement with selection
- if key == "left" or key == "right" or key == "home" or key == "end" or key == "up" or key == "down" then
- if modifiers.shift and not self._selectionAnchor then
- self._selectionAnchor = self._cursorPosition
- end
-
- if key == "left" then
- if modifiers.super then
- self:moveCursorToStart(element)
- if not modifiers.shift then
- self:clearSelection()
- end
- elseif modifiers.alt then
- self:moveCursorToPreviousWord()
- elseif self:hasSelection() and not modifiers.shift then
- local startPos, _ = self:getSelection()
- self._cursorPosition = startPos
- self:clearSelection()
- else
- self:moveCursorBy(element, -1)
- end
- elseif key == "right" then
- if modifiers.super then
- self:moveCursorToEnd(element)
- if not modifiers.shift then
- self:clearSelection()
- end
- elseif modifiers.alt then
- self:moveCursorToNextWord()
- elseif self:hasSelection() and not modifiers.shift then
- local _, endPos = self:getSelection()
- self._cursorPosition = endPos
- self:clearSelection()
- else
- self:moveCursorBy(element, 1)
- end
- elseif key == "home" then
- if not self.multiline then
- self:moveCursorToStart(element)
- else
- self:moveCursorToLineStart(element)
- end
- if not modifiers.shift then
- self:clearSelection()
- end
- elseif key == "end" then
- if not self.multiline then
- self:moveCursorToEnd(element)
- else
- self:moveCursorToLineEnd(element)
- end
- if not modifiers.shift then
- self:clearSelection()
- end
- elseif key == "up" or key == "down" then
- if not modifiers.shift then
- self:clearSelection()
- end
- end
-
- -- Update selection if Shift is pressed
- if modifiers.shift and self._selectionAnchor then
- self:setSelection(element, self._selectionAnchor, self._cursorPosition)
- elseif not modifiers.shift then
- self._selectionAnchor = nil
- end
-
- self:_resetCursorBlink(element)
-
- -- Handle backspace and delete
- elseif key == "backspace" then
- local oldText = self._textBuffer
- if self:hasSelection() then
- self:deleteSelection(element)
- elseif ctrl then
- if self._cursorPosition > 0 then
- self:deleteText(element, 0, self._cursorPosition)
- self._cursorPosition = 0
- self:_validateCursorPosition()
- end
- elseif self._cursorPosition > 0 then
- local deleteStart = self._cursorPosition - 1
- local deleteEnd = self._cursorPosition
- self._cursorPosition = deleteStart
- self:deleteText(element, deleteStart, deleteEnd)
- self:_validateCursorPosition()
- end
-
- if self.onTextChange and self._textBuffer ~= oldText then
- self.onTextChange(element, self._textBuffer, oldText)
- end
- self:_resetCursorBlink(element, true)
- elseif key == "delete" then
- local oldText = self._textBuffer
- if self:hasSelection() then
- self:deleteSelection(element)
- else
- local textLength = utf8.len(self._textBuffer or "")
- if self._cursorPosition < textLength then
- self:deleteText(element, self._cursorPosition, self._cursorPosition + 1)
- end
- end
-
- if self.onTextChange and self._textBuffer ~= oldText then
- self.onTextChange(element, self._textBuffer, oldText)
- end
- self:_resetCursorBlink(element, true)
-
- -- Handle return/enter
- elseif key == "return" or key == "kpenter" then
- if self.multiline then
- local oldText = self._textBuffer
- if self:hasSelection() then
- self:deleteSelection(element)
- end
- self:insertText(element, "\n")
-
- if self.onTextChange and self._textBuffer ~= oldText then
- self.onTextChange(element, self._textBuffer, oldText)
- end
- else
- if self.onEnter then
- self.onEnter(element)
- end
- end
- self:_resetCursorBlink(element, true)
-
- -- Handle Ctrl/Cmd+A (select all)
- elseif ctrl and key == "a" then
- self:selectAll(element)
- self:_resetCursorBlink(element)
-
- -- Handle Ctrl/Cmd+C (copy)
- elseif ctrl and key == "c" then
- if self:hasSelection() then
- local selectedText = self:getSelectedText()
- if selectedText then
- love.system.setClipboardText(selectedText)
- end
- end
- self:_resetCursorBlink(element)
-
- -- Handle Ctrl/Cmd+X (cut)
- elseif ctrl and key == "x" then
- if self:hasSelection() then
- local selectedText = self:getSelectedText()
- if selectedText then
- love.system.setClipboardText(selectedText)
-
- local oldText = self._textBuffer
- self:deleteSelection(element)
-
- if self.onTextChange and self._textBuffer ~= oldText then
- self.onTextChange(element, self._textBuffer, oldText)
- end
- end
- end
- self:_resetCursorBlink(element, true)
-
- -- Handle Ctrl/Cmd+V (paste)
- elseif ctrl and key == "v" then
- local clipboardText = love.system.getClipboardText()
- if clipboardText and clipboardText ~= "" then
- local oldText = self._textBuffer
-
- if self:hasSelection() then
- self:deleteSelection(element)
- end
-
- self:insertText(element, clipboardText)
-
- if self.onTextChange and self._textBuffer ~= oldText then
- self.onTextChange(element, self._textBuffer, oldText)
- end
- end
- self:_resetCursorBlink(element, true)
-
- -- Handle Escape
- elseif key == "escape" then
- if self:hasSelection() then
- self:clearSelection()
- else
- self:blur(element)
- end
- self:_resetCursorBlink(element)
- end
-
- self:_saveState(element)
-end
-
--- ====================
--- Mouse Input
--- ====================
-
----Convert mouse coordinates to cursor position in text
----@param element Element The parent element
----@param mouseX number -- Mouse X coordinate (absolute)
----@param mouseY number -- Mouse Y coordinate (absolute)
----@return number -- Cursor position (character index)
-function TextEditor:mouseToTextPosition(element, mouseX, mouseY)
- if not element or not self._textBuffer then
- return 0
- end
-
- local font = self:_getFont(element)
- if not font then
- return 0
- end
-
- -- Get content area bounds
- local contentX = (element._absoluteX or element.x) + element.padding.left
- local contentY = (element._absoluteY or element.y) + element.padding.top
-
- -- Calculate relative position
- local relativeX = mouseX - contentX
- local relativeY = mouseY - contentY
-
- local text = self._textBuffer
- local textLength = utf8.len(text) or 0
-
- -- Single-line handling
- if not self.multiline then
- if self._textScrollX then
- relativeX = relativeX + self._textScrollX
- end
-
- local closestPos = 0
- local closestDist = math.huge
-
- for i = 0, textLength do
- local offset = utf8.offset(text, i + 1)
- local beforeText = offset and text:sub(1, offset - 1) or text
- local textWidth = font:getWidth(beforeText)
- local dist = math.abs(relativeX - textWidth)
-
- if dist < closestDist then
- closestDist = dist
- closestPos = i
- end
- end
-
- return closestPos
- end
-
- -- Multiline handling
- self:_updateTextIfDirty(element)
-
- -- Split text into lines
- local lines = {}
- for line in (text .. "\n"):gmatch("([^\n]*)\n") do
- table.insert(lines, line)
- end
- if #lines == 0 then
- lines = { "" }
- end
-
- local lineHeight = font:getHeight()
-
- -- Get text area width
- local textAreaWidth = element.width
- local scaledContentPadding = element:getScaledContentPadding()
- if scaledContentPadding then
- local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
- end
-
- -- Determine which line was clicked
- local clickedLineNum = math.floor(relativeY / lineHeight) + 1
- clickedLineNum = math.max(1, math.min(clickedLineNum, #lines))
-
- -- Calculate character offset for lines before clicked line
- local charOffset = 0
- for i = 1, clickedLineNum - 1 do
- local lineLen = utf8.len(lines[i]) or 0
- charOffset = charOffset + lineLen + 1
- end
-
- local clickedLine = lines[clickedLineNum]
- local lineLen = utf8.len(clickedLine) or 0
-
- -- Handle wrapped segments
- if self.textWrap and textAreaWidth > 0 then
- local wrappedSegments = self:_wrapLine(element, clickedLine, textAreaWidth)
- local lineYOffset = (clickedLineNum - 1) * lineHeight
- local segmentNum = math.floor((relativeY - lineYOffset) / lineHeight) + 1
- segmentNum = math.max(1, math.min(segmentNum, #wrappedSegments))
-
- local segment = wrappedSegments[segmentNum]
- local segmentText = segment.text
- local segmentLen = utf8.len(segmentText) or 0
- local closestPos = segment.startIdx
- local closestDist = math.huge
-
- for i = 0, segmentLen do
- local offset = utf8.offset(segmentText, i + 1)
- local beforeText = offset and segmentText:sub(1, offset - 1) or segmentText
- local textWidth = font:getWidth(beforeText)
- local dist = math.abs(relativeX - textWidth)
-
- if dist < closestDist then
- closestDist = dist
- closestPos = segment.startIdx + i
- end
- end
-
- return charOffset + closestPos
- end
-
- -- No wrapping
- local closestPos = 0
- local closestDist = math.huge
-
- for i = 0, lineLen do
- local offset = utf8.offset(clickedLine, i + 1)
- local beforeText = offset and clickedLine:sub(1, offset - 1) or clickedLine
- local textWidth = font:getWidth(beforeText)
- local dist = math.abs(relativeX - textWidth)
-
- if dist < closestDist then
- closestDist = dist
- closestPos = i
- end
- end
-
- return charOffset + closestPos
-end
-
----Handle mouse click on text
----@param element Element The parent element
----@param mouseX number
----@param mouseY number
----@param clickCount number -- 1=single, 2=double, 3=triple
-function TextEditor:handleTextClick(element, mouseX, mouseY, clickCount)
- if not self._focused then
- return
- end
-
- if clickCount == 1 then
- local pos = self:mouseToTextPosition(element, mouseX, mouseY)
- self:setCursorPosition(element, pos)
- self:clearSelection()
- self._mouseDownPosition = pos
- elseif clickCount == 2 then
- self:_selectWordAtPosition(element, self:mouseToTextPosition(element, mouseX, mouseY))
- elseif clickCount >= 3 then
- self:selectAll(element)
- end
-
- self:_resetCursorBlink(element)
-end
-
----Handle mouse drag for text selection
----@param element Element The parent element
----@param mouseX number
----@param mouseY number
-function TextEditor:handleTextDrag(element, mouseX, mouseY)
- if not self._focused or not element._mouseDownPosition then
- return
- end
-
- local currentPos = self:mouseToTextPosition(element, mouseX, mouseY)
-
- if currentPos ~= element._mouseDownPosition then
- self:setSelection(element, element._mouseDownPosition, currentPos)
- self._cursorPosition = currentPos
- self._textDragOccurred = true
- else
- self:clearSelection()
- end
-
- self:_resetCursorBlink(element)
-end
-
----Select word at given position
----@param element Element? The parent element (for scroll updates)
----@param position number
-function TextEditor:_selectWordAtPosition(element, position)
- if not self._textBuffer then
- return
- end
-
- local text = self._textBuffer
- local textLength = utf8.len(text) or 0
-
- if textLength == 0 then
- return
- end
-
- -- Helper to get character at position
- local function getCharAt(p)
- if p < 0 or p >= textLength then
- return nil
- end
- local offset1 = utf8.offset(text, p + 1)
- local offset2 = utf8.offset(text, p + 2)
- if not offset1 then
- return nil
- end
- if not offset2 then
- return text:sub(offset1)
- end
- return text:sub(offset1, offset2 - 1)
- end
-
- -- Find word boundaries
- local startPos = position
- local endPos = position
-
- -- Expand left to start of word
- while startPos > 0 do
- local char = getCharAt(startPos - 1)
- if not char or not char:match("[%w]") then
- break
- end
- startPos = startPos - 1
- end
-
- -- Expand right to end of word
- while endPos < textLength do
- local char = getCharAt(endPos)
- if not char or not char:match("[%w]") then
- break
- end
- endPos = endPos + 1
- end
-
- self:setSelection(element, startPos, endPos)
- self._cursorPosition = endPos
-end
-
--- ====================
--- Update and Rendering
--- ====================
-
----Update cursor blink animation
----@param element Element The parent element
----@param dt number -- Delta time
-function TextEditor:update(element, dt)
- if not self._focused then
- return
- end
-
- -- Update cursor blink
- if self._cursorBlinkPaused then
- self._cursorBlinkPauseTimer = (self._cursorBlinkPauseTimer or 0) + dt
- if self._cursorBlinkPauseTimer >= 0.5 then
- self._cursorBlinkPaused = false
- self._cursorBlinkPauseTimer = 0
- end
- else
- self._cursorBlinkTimer = self._cursorBlinkTimer + dt
- if self._cursorBlinkTimer >= self.cursorBlinkRate then
- self._cursorBlinkTimer = 0
- self._cursorVisible = not self._cursorVisible
- end
- end
-
- -- Save state for immediate mode (cursor blink timer changes need to persist)
- self:_saveState(element)
-end
-
----Update element height based on text content (for autoGrow)
----@param element Element The parent element
-function TextEditor:updateAutoGrowHeight(element)
- if not self.multiline or not self.autoGrow or not element then
- return
- end
-
- local font = self:_getFont(element)
- if not font then
- return
- end
-
- local text = self._textBuffer or ""
- local lineHeight = font:getHeight()
-
- -- Get text area width
- local textAreaWidth = element.width
- local scaledContentPadding = element:getScaledContentPadding()
- if scaledContentPadding then
- local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
- end
-
- -- Split text by newlines
- local lines = {}
- for line in (text .. "\n"):gmatch("([^\n]*)\n") do
- table.insert(lines, line)
- end
- if #lines == 0 then
- lines = { "" }
- end
-
- -- Count total wrapped lines
- local totalWrappedLines = 0
- if self.textWrap and textAreaWidth > 0 then
- for _, line in ipairs(lines) do
- if line == "" then
- totalWrappedLines = totalWrappedLines + 1
- else
- local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)
- totalWrappedLines = totalWrappedLines + #wrappedSegments
- end
- end
- else
- totalWrappedLines = #lines
- end
-
- totalWrappedLines = math.max(1, totalWrappedLines)
- local newContentHeight = totalWrappedLines * lineHeight
-
- if element.height ~= newContentHeight then
- element.height = newContentHeight
- element._borderBoxHeight = element.height + element.padding.top + element.padding.bottom
- if element.parent and not element._explicitlyAbsolute then
- element.parent:layoutChildren()
- end
- end
-end
-
--- ====================
--- Helper Methods
--- ====================
-
----Get font for text rendering
----@param element Element? The parent element
----@return love.Font?
-function TextEditor:_getFont(element)
- if not element then
- return nil
- end
-
- -- Delegate to Renderer
- return element._renderer:getFont(element)
-end
-
---- Get current state for persistence
----@return table state TextEditor state snapshot
-function TextEditor:getState()
- return {
- _cursorPosition = self._cursorPosition,
- _selectionStart = self._selectionStart,
- _selectionEnd = self._selectionEnd,
- _textBuffer = self._textBuffer,
- _cursorBlinkTimer = self._cursorBlinkTimer,
- _cursorVisible = self._cursorVisible,
- _cursorBlinkPaused = self._cursorBlinkPaused,
- _cursorBlinkPauseTimer = self._cursorBlinkPauseTimer,
- _focused = self._focused,
- }
-end
-
---- Restore state from persistence
----@param state table State to restore
----@param element Element? The parent element (needed for focus restoration)
-function TextEditor:setState(state, element)
- if not state then
- return
- end
-
- if state._cursorPosition ~= nil then
- self._cursorPosition = state._cursorPosition
- end
-
- if state._selectionStart ~= nil then
- self._selectionStart = state._selectionStart
- end
-
- if state._selectionEnd ~= nil then
- self._selectionEnd = state._selectionEnd
- end
-
- if state._textBuffer ~= nil then
- self._textBuffer = state._textBuffer
- end
-
- if state._cursorBlinkTimer ~= nil then
- self._cursorBlinkTimer = state._cursorBlinkTimer
- end
-
- if state._cursorVisible ~= nil then
- self._cursorVisible = state._cursorVisible
- end
-
- if state._cursorBlinkPaused ~= nil then
- self._cursorBlinkPaused = state._cursorBlinkPaused
- end
-
- if state._cursorBlinkPauseTimer ~= nil then
- self._cursorBlinkPauseTimer = state._cursorBlinkPauseTimer
- end
-
- if state._focused ~= nil then
- self._focused = state._focused
- -- Restore focused element in Context if this element was focused
- if self._focused and element then
- self._Context.setFocused(element)
- end
- end
-end
-
----Save state to StateManager (for immediate mode)
----@param element Element? The parent element
-function TextEditor:_saveState(element)
- -- Mode-aware guard: in retained mode the TextEditor persists, so state only
- -- needs persisting to StateManager in immediate mode. Routed through
- -- Context.isImmediateMode (behavior-mode-unification task 11).
- if not element or not element._stateId or not self._Context.isImmediateMode() then
- return
- end
-
- -- Get current state (may have other sub-modules like eventHandler, scrollManager)
- local currentState = self._StateManager.getState(element._stateId) or {}
-
- -- Update only the textEditor sub-table to match the nested structure
- -- used by element:saveState() at endFrame
- currentState.textEditor = {
- _focused = self._focused,
- _textBuffer = self._textBuffer,
- _cursorPosition = self._cursorPosition,
- _selectionStart = self._selectionStart,
- _selectionEnd = self._selectionEnd,
- _cursorBlinkTimer = self._cursorBlinkTimer,
- _cursorVisible = self._cursorVisible,
- _cursorBlinkPaused = self._cursorBlinkPaused,
- _cursorBlinkPauseTimer = self._cursorBlinkPauseTimer,
- }
-
- self._StateManager.updateState(element._stateId, currentState)
-end
-
-return TextEditor
diff --git a/libs/flexlove/modules/TextSanitizer.lua b/libs/flexlove/modules/TextSanitizer.lua
deleted file mode 100644
index e7e57d12..00000000
--- a/libs/flexlove/modules/TextSanitizer.lua
+++ /dev/null
@@ -1,183 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local function req(name)
- return require(modulePath .. name)
-end
-
--- Text sanitization, escaping, and input validation utilities.
-
--- ErrorHandler is injected via init() for truncation warnings.
-local ErrorHandler = nil
-
---- Initialize dependencies
----@param deps table Dependencies: { ErrorHandler = ErrorHandler }
-local function init(deps)
- if type(deps) == "table" then
- ErrorHandler = deps.ErrorHandler
- end
-end
-
---- Sanitize text to prevent security vulnerabilities
---- @param text string? Text to sanitize
---- @param options table? Sanitization options
---- @return string Sanitized text
-local function sanitizeText(text, options)
- local utf8 = require("utf8")
- -- Handle nil or non-string inputs
- if text == nil then
- return ""
- end
- if type(text) ~= "string" then
- text = tostring(text)
- end
-
- -- Default options
- options = options or {}
- local maxLength = options.maxLength or 10000
- local allowNewlines = options.allowNewlines ~= false -- default true
- local allowTabs = options.allowTabs ~= false -- default true
- local stripControls = options.stripControls ~= false -- default true
- local trimWhitespace = options.trimWhitespace ~= false -- default true
-
- -- Remove null bytes (critical security risk)
- text = text:gsub("%z", "")
-
- -- Strip control characters except allowed ones
- if stripControls then
- local pattern = "[\1-\31\127]" -- All control characters
- if allowNewlines and allowTabs then
- pattern = "[\1-\8\11\12\14-\31\127]" -- Exclude \t (9), \n (10), \r (13)
- elseif allowNewlines then
- pattern = "[\1-\9\11\12\14-\31\127]" -- Exclude \n (10), \r (13)
- elseif allowTabs then
- pattern = "[\1-\8\10\12-\31\127]" -- Exclude \t (9)
- end
- text = text:gsub(pattern, "")
- end
-
- -- Trim leading/trailing whitespace
- if trimWhitespace then
- text = text:match("^%s*(.-)%s*$") or ""
- end
-
- -- Limit string length (use UTF-8 character count, not byte count)
- local charCount = utf8.len(text)
- if charCount and charCount > maxLength then
- if ErrorHandler then
- ErrorHandler:warn("utils", "UTIL_001", {
- original = charCount,
- truncated = maxLength,
- })
- end
- -- Truncate to maxLength UTF-8 characters
- local bytePos = utf8.offset(text, maxLength + 1)
- if bytePos then
- text = text:sub(1, bytePos - 1)
- end
- if ErrorHandler then
- ErrorHandler:warn("utils", string.format("Text truncated from %d to %d characters", charCount, maxLength))
- end
- end
-
- return text
-end
-
---- Validate text input against rules
---- @param text string Text to validate
---- @param rules table Validation rules
---- @return boolean, string? Returns true if valid, or false with error message
-local function validateTextInput(text, rules)
- rules = rules or {}
-
- -- Check minimum length
- if rules.minLength and #text < rules.minLength then
- return false, string.format("Text must be at least %d characters", rules.minLength)
- end
-
- -- Check maximum length
- if rules.maxLength and #text > rules.maxLength then
- return false, string.format("Text must be at most %d characters", rules.maxLength)
- end
-
- -- Check pattern match
- if rules.pattern and not text:match(rules.pattern) then
- return false, rules.patternError or "Text does not match required pattern"
- end
-
- -- Check character whitelist
- if rules.allowedChars then
- local pattern = "[^" .. rules.allowedChars .. "]"
- if text:match(pattern) then
- return false, "Text contains invalid characters"
- end
- end
-
- -- Check character blacklist
- if rules.forbiddenChars then
- local pattern = "[" .. rules.forbiddenChars .. "]"
- if text:match(pattern) then
- return false, "Text contains forbidden characters"
- end
- end
-
- return true, nil
-end
-
---- Validate text against range/length rules (alias of validateTextInput)
---- @param text string Text to validate
---- @param rules table Validation rules (minLength, maxLength, pattern, etc.)
---- @return boolean, string? Returns true if valid, or false with error message
-local function validateTextRange(text, rules)
- return validateTextInput(text, rules)
-end
-
---- Escape HTML special characters
---- @param text string Text to escape
---- @return string Escaped text
-local function escapeHtml(text)
- if text == nil then
- return ""
- end
- text = tostring(text)
- text = text:gsub("&", "&")
- text = text:gsub("<", "<")
- text = text:gsub(">", ">")
- text = text:gsub('"', """)
- text = text:gsub("'", "'")
- return text
-end
-
---- Escape Lua pattern special characters
---- @param text string Text to escape
---- @return string Escaped text
-local function escapeLuaPattern(text)
- if text == nil then
- return ""
- end
- text = tostring(text)
- -- Escape all Lua pattern special characters
- text = text:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
- return text
-end
-
---- Strip all non-printable characters from text
---- @param text string Text to clean
---- @return string Cleaned text
-local function stripNonPrintable(text)
- if text == nil then
- return ""
- end
- text = tostring(text)
- -- Keep printable ASCII (32-126), newline (10), tab (9), and carriage return (13)
- text = text:gsub("[^\9\10\13\32-\126]", "")
- return text
-end
-
-return {
- init = init,
- sanitizeText = sanitizeText,
- validateTextInput = validateTextInput,
- validateTextRange = validateTextRange,
- escapeHtml = escapeHtml,
- escapeLuaPattern = escapeLuaPattern,
- stripNonPrintable = stripNonPrintable,
-}
diff --git a/libs/flexlove/modules/Theme.lua b/libs/flexlove/modules/Theme.lua
deleted file mode 100644
index f6e95bfe..00000000
--- a/libs/flexlove/modules/Theme.lua
+++ /dev/null
@@ -1,1655 +0,0 @@
---- Auto-detect the base path where FlexLove is located
----@return string modulePath, string filesystemPath
-local function getFlexLoveBasePath()
- -- Get debug info to find where this file is loaded from
- local info = debug.getinfo(1, "S")
- if info and info.source then
- local source = info.source
- -- Remove leading @ if present
- if source:sub(1, 1) == "@" then
- source = source:sub(2)
- end
-
- -- Extract the directory path (remove Theme.lua and modules/)
- local filesystemPath = source:match("(.*/)")
- if filesystemPath then
- -- Store the original filesystem path for loading assets
- local fsPath = filesystemPath
- -- Remove leading ./ if present
- fsPath = fsPath:gsub("^%./", "")
- -- Remove trailing /
- fsPath = fsPath:gsub("/$", "")
- -- Remove the flexlove subdirectory to get back to base
- fsPath = fsPath:gsub("/modules$", "")
-
- -- Convert filesystem path to Lua module path
- local modulePath = fsPath:gsub("/", ".")
-
- return modulePath, fsPath
- end
- end
-
- -- Fallback: try a common path
- return "libs", "libs"
-end
-
--- Store the base paths when module loads
-local FLEXLOVE_BASE_PATH, FLEXLOVE_FILESYSTEM_PATH = getFlexLoveBasePath()
-
---- Validate theme definition structure
----@param definition ThemeDefinition
----@return boolean, string? -- Returns true if valid, or false with error message
-local function validateThemeDefinition(definition)
- if not definition then
- return false, "Theme definition is nil"
- end
-
- if type(definition) ~= "table" then
- return false, "Theme definition must be a table"
- end
-
- if not definition.name or type(definition.name) ~= "string" then
- return false, "Theme must have a 'name' field (string)"
- end
-
- if definition.components and type(definition.components) ~= "table" then
- return false, "Theme 'components' must be a table"
- end
-
- if definition.colors and type(definition.colors) ~= "table" then
- return false, "Theme 'colors' must be a table"
- end
-
- if definition.fonts and type(definition.fonts) ~= "table" then
- return false, "Theme 'fonts' must be a table"
- end
-
- if definition.scrollbars and type(definition.scrollbars) ~= "table" then
- return false, "Theme 'scrollbars' must be a table"
- end
-
- return true, nil
-end
-
---- Load image data from a file path
----@param imagePath string
----@return love.ImageData
-local function loadImageData(imagePath)
- if not imagePath then
- error("Image path cannot be nil")
- end
-
- local success, result = pcall(function()
- return love.image.newImageData(imagePath)
- end)
-
- if not success then
- error("Failed to load image data from '" .. imagePath .. "': " .. tostring(result))
- end
-
- return result
-end
-
---- Extract all pixels from a specific row
----@param imageData love.ImageData
----@param rowIndex number 0-based row index
----@return table Array of {r, g, b, a} values (0-255 range)
-local function getRow(imageData, rowIndex)
- if not imageData then
- error("ImageData cannot be nil")
- end
-
- local width = imageData:getWidth()
- local height = imageData:getHeight()
-
- if rowIndex < 0 or rowIndex >= height then
- error(string.format("Row index %d out of bounds (height: %d)", rowIndex, height))
- end
-
- local pixels = {}
- for x = 0, width - 1 do
- local r, g, b, a = imageData:getPixel(x, rowIndex)
- table.insert(pixels, {
- r = math.floor(r * 255 + 0.5),
- g = math.floor(g * 255 + 0.5),
- b = math.floor(b * 255 + 0.5),
- a = math.floor(a * 255 + 0.5),
- })
- end
-
- return pixels
-end
-
---- Extract all pixels from a specific column
----@param imageData love.ImageData
----@param colIndex number 0-based column index
----@return table Array of {r, g, b, a} values (0-255 range)
-local function getColumn(imageData, colIndex)
- if not imageData then
- error("ImageData cannot be nil")
- end
-
- local width = imageData:getWidth()
- local height = imageData:getHeight()
-
- if colIndex < 0 or colIndex >= width then
- error(string.format("Column index %d out of bounds (width: %d)", colIndex, width))
- end
-
- local pixels = {}
- for y = 0, height - 1 do
- local r, g, b, a = imageData:getPixel(colIndex, y)
- table.insert(pixels, {
- r = math.floor(r * 255 + 0.5),
- g = math.floor(g * 255 + 0.5),
- b = math.floor(b * 255 + 0.5),
- a = math.floor(a * 255 + 0.5),
- })
- end
-
- return pixels
-end
-
---- Check if a pixel is black with full alpha (9-patch marker)
----@param r number Red (0-255)
----@param g number Green (0-255)
----@param b number Blue (0-255)
----@param a number Alpha (0-255)
----@return boolean
-local function isBlackPixel(r, g, b, a)
- return r == 0 and g == 0 and b == 0 and a == 255
-end
-
---- Find all continuous runs of black pixels in a pixel array
----@param pixels table Array of {r, g, b, a} pixel values
----@return table Array of {start, end} pairs (1-based indices, inclusive)
-local function findBlackPixelRuns(pixels)
- local runs = {}
- local inRun = false
- local runStart = nil
-
- for i = 1, #pixels do
- local pixel = pixels[i]
- local isBlack = isBlackPixel(pixel.r, pixel.g, pixel.b, pixel.a)
-
- if isBlack and not inRun then
- -- Start of a new run
- inRun = true
- runStart = i
- elseif not isBlack and inRun then
- -- End of current run
- table.insert(runs, { start = runStart, ["end"] = i - 1 })
- inRun = false
- runStart = nil
- end
- end
-
- -- Handle case where run extends to end of array
- if inRun then
- table.insert(runs, { start = runStart, ["end"] = #pixels })
- end
-
- return runs
-end
-
---- Parse a 9-patch PNG image to extract stretch regions and content padding
----@param imagePath string Path to the 9-patch image file
----@return table|nil, string|nil Returns {insets, stretchX, stretchY} or nil, error message
-local function parseNinePatch(imagePath)
- if not imagePath then
- return nil, "Image path cannot be nil"
- end
-
- local success, imageData = pcall(function()
- return loadImageData(imagePath)
- end)
-
- if not success then
- return nil, "Failed to load image data: " .. tostring(imageData)
- end
-
- local width = imageData:getWidth()
- local height = imageData:getHeight()
-
- -- Validate minimum size (must be at least 3x3 with 1px border)
- if width < 3 or height < 3 then
- return nil, string.format("Invalid 9-patch dimensions: %dx%d (minimum 3x3)", width, height)
- end
-
- -- Extract border pixels (0-based indexing, but we convert to 1-based for processing)
- local topBorder = getRow(imageData, 0)
- local leftBorder = getColumn(imageData, 0)
- local bottomBorder = getRow(imageData, height - 1)
- local rightBorder = getColumn(imageData, width - 1)
-
- -- Remove corner pixels from borders (they're not part of the stretch/content markers)
- -- Top and bottom borders: remove first and last pixel
- local topStretchPixels = {}
- local bottomContentPixels = {}
- for i = 2, #topBorder - 1 do
- table.insert(topStretchPixels, topBorder[i])
- end
- for i = 2, #bottomBorder - 1 do
- table.insert(bottomContentPixels, bottomBorder[i])
- end
-
- -- Left and right borders: remove first and last pixel
- local leftStretchPixels = {}
- local rightContentPixels = {}
- for i = 2, #leftBorder - 1 do
- table.insert(leftStretchPixels, leftBorder[i])
- end
- for i = 2, #rightBorder - 1 do
- table.insert(rightContentPixels, rightBorder[i])
- end
-
- -- Find stretch regions (top and left borders)
- local stretchX = findBlackPixelRuns(topStretchPixels)
- local stretchY = findBlackPixelRuns(leftStretchPixels)
-
- -- Find content padding regions (bottom and right borders)
- local contentX = findBlackPixelRuns(bottomContentPixels)
- local contentY = findBlackPixelRuns(rightContentPixels)
-
- -- Validate that we have at least one stretch region
- if #stretchX == 0 or #stretchY == 0 then
- return nil, "No stretch regions found (top or left border has no black pixels)"
- end
-
- -- Calculate stretch insets from stretch regions (top/left guides)
- -- Use the first stretch region's start and last stretch region's end
- local firstStretchX = stretchX[1]
- local lastStretchX = stretchX[#stretchX]
- local firstStretchY = stretchY[1]
- local lastStretchY = stretchY[#stretchY]
-
- -- Stretch insets define the 9-patch regions
- local stretchLeft = firstStretchX.start
- local stretchRight = #topStretchPixels - lastStretchX["end"]
- local stretchTop = firstStretchY.start
- local stretchBottom = #leftStretchPixels - lastStretchY["end"]
-
- -- Calculate content padding from content guides (bottom/right guides)
- -- If content padding is defined, use it; otherwise use stretch regions
- local contentLeft, contentRight, contentTop, contentBottom
-
- if #contentX > 0 then
- contentLeft = contentX[1].start
- contentRight = #topStretchPixels - contentX[#contentX]["end"]
- else
- contentLeft = stretchLeft
- contentRight = stretchRight
- end
-
- if #contentY > 0 then
- contentTop = contentY[1].start
- contentBottom = #leftStretchPixels - contentY[#contentY]["end"]
- else
- contentTop = stretchTop
- contentBottom = stretchBottom
- end
-
- return {
- insets = {
- left = stretchLeft,
- top = stretchTop,
- right = stretchRight,
- bottom = stretchBottom,
- },
- contentPadding = {
- left = contentLeft,
- top = contentTop,
- right = contentRight,
- bottom = contentBottom,
- },
- stretchX = stretchX,
- stretchY = stretchY,
- }
-end
-
----@class Theme
-local Theme = {}
-Theme.__index = Theme
-
---- Initialize module with shared dependencies
----@param deps table Dependencies {ErrorHandler, Color, utils}
-function Theme.init(deps)
- if type(deps) == "table" then
- Theme._ErrorHandler = deps.ErrorHandler
- Theme._Color = deps.Color
- Theme._utils = deps.utils
- end
-end
-
--- Global theme registry
-local themes = {}
-local activeTheme = nil
-
---- Create reusable design systems with consistent styling, 9-patch assets, and component states
---- Use this to build professional-looking UIs with minimal per-element configuration
----@param definition ThemeDefinition Theme definition table
----@return Theme theme The new theme instance
-function Theme.new(definition)
- -- Validate input type first
- if type(definition) ~= "table" then
- Theme._ErrorHandler:warn("Theme", "THM_001", {
- error = "Theme definition must be a table, got " .. type(definition),
- })
- return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} })
- end
-
- -- Validate theme definition
- local valid, err = validateThemeDefinition(definition)
- if not valid then
- Theme._ErrorHandler:warn("Theme", "THM_001", {
- error = tostring(err),
- })
- return Theme.new({ name = "fallback", components = {}, colors = {}, fonts = {} })
- end
-
- local self = setmetatable({}, Theme)
- self.name = definition.name
-
- -- Load global atlas if it's a string path
- if definition.atlas then
- if type(definition.atlas) == "string" then
- local resolvedPath = Theme._utils.resolveImagePath(definition.atlas)
- local image, imageData, loaderr = Theme._utils.safeLoadImage(resolvedPath)
- if image then
- self.atlas = image
- self.atlasData = imageData
- else
- Theme._ErrorHandler:warn("Theme", "RES_001", {
- theme = definition.name,
- path = resolvedPath,
- error = loaderr,
- })
- end
- else
- self.atlas = definition.atlas
- end
- end
-
- self.components = definition.components or {}
- self.scrollbars = definition.scrollbars or {}
- self.colors = definition.colors or {}
- self.fonts = definition.fonts or {}
- self.contentAutoSizingMultiplier = definition.contentAutoSizingMultiplier or nil
-
- -- Helper function to strip 1-pixel guide border from 9-patch ImageData
- ---@param sourceImageData love.ImageData
- ---@return love.ImageData -- New ImageData without guide border
- local function stripNinePatchBorder(sourceImageData)
- local srcWidth = sourceImageData:getWidth()
- local srcHeight = sourceImageData:getHeight()
-
- -- Content dimensions (excluding 1px border on all sides)
- local contentWidth = srcWidth - 2
- local contentHeight = srcHeight - 2
-
- if contentWidth <= 0 or contentHeight <= 0 then
- Theme._ErrorHandler:warn("Theme", "RES_002", {
- width = srcWidth,
- height = srcHeight,
- reason = "Image must be larger than 2x2 pixels to have content after stripping 1px border",
- })
- return nil
- end
-
- -- Create new ImageData for content only
- local strippedImageData = love.image.newImageData(contentWidth, contentHeight)
-
- -- Copy pixels from source (1,1) to (width-2, height-2)
- for y = 0, contentHeight - 1 do
- for x = 0, contentWidth - 1 do
- local r, g, b, a = sourceImageData:getPixel(x + 1, y + 1)
- strippedImageData:setPixel(x, y, r, g, b, a)
- end
- end
-
- return strippedImageData
- end
-
- -- Helper function to load atlas with 9-patch support
- local function loadAtlasWithNinePatch(comp, atlasPath, errorContext)
- ---@diagnostic disable-next-line
- local resolvedPath = Theme._utils.resolveImagePath(atlasPath)
- ---@diagnostic disable-next-line
- local is9Patch = not comp.insets and atlasPath:match("%.9%.png$")
-
- if is9Patch then
- local parseResult, parseErr = parseNinePatch(resolvedPath)
- if parseResult then
- comp.insets = parseResult.insets
- comp._ninePatchData = parseResult
- else
- Theme._ErrorHandler:warn("Theme", "RES_003", {
- context = errorContext,
- path = resolvedPath,
- error = tostring(parseErr),
- })
- end
- end
-
- local image, imageData, loaderr = Theme._utils.safeLoadImage(resolvedPath)
- if image then
- -- Strip guide border for 9-patch images
- if is9Patch and imageData then
- local strippedImageData = stripNinePatchBorder(imageData)
- local strippedImage = love.graphics.newImage(strippedImageData)
- comp._loadedAtlas = strippedImage
- comp._loadedAtlasData = strippedImageData
- else
- comp._loadedAtlas = image
- comp._loadedAtlasData = imageData
- end
- else
- Theme._ErrorHandler:warn("Theme", "RES_001", {
- context = errorContext,
- path = resolvedPath,
- error = tostring(loaderr),
- })
- end
- end
-
- -- Helper function to create regions from insets
- local function createRegionsFromInsets(comp, fallbackAtlas)
- local atlasImage = comp._loadedAtlas or fallbackAtlas
- if not atlasImage or type(atlasImage) == "string" then
- return
- end
-
- local imgWidth, imgHeight = atlasImage:getDimensions()
- local left = comp.insets.left or 0
- local top = comp.insets.top or 0
- local right = comp.insets.right or 0
- local bottom = comp.insets.bottom or 0
-
- -- No offsets needed - guide border has been stripped for 9-patch images
- local centerWidth = imgWidth - left - right
- local centerHeight = imgHeight - top - bottom
-
- comp.regions = {
- topLeft = { x = 0, y = 0, w = left, h = top },
- topCenter = { x = left, y = 0, w = centerWidth, h = top },
- topRight = { x = left + centerWidth, y = 0, w = right, h = top },
- middleLeft = { x = 0, y = top, w = left, h = centerHeight },
- middleCenter = { x = left, y = top, w = centerWidth, h = centerHeight },
- middleRight = { x = left + centerWidth, y = top, w = right, h = centerHeight },
- bottomLeft = { x = 0, y = top + centerHeight, w = left, h = bottom },
- bottomCenter = { x = left, y = top + centerHeight, w = centerWidth, h = bottom },
- bottomRight = { x = left + centerWidth, y = top + centerHeight, w = right, h = bottom },
- }
- end
-
- -- Load component-specific atlases and process 9-patch definitions
- for componentName, component in pairs(self.components) do
- if component.atlas then
- if type(component.atlas) == "string" then
- loadAtlasWithNinePatch(component, component.atlas, "for component '" .. componentName .. "'")
- else
- -- Direct Image object (no ImageData available - scaleCorners won't work)
- component._loadedAtlas = component.atlas
- end
- end
-
- if component.insets then
- createRegionsFromInsets(component, self.atlas)
- end
-
- if component.states then
- for stateName, stateComponent in pairs(component.states) do
- if stateComponent.atlas then
- if type(stateComponent.atlas) == "string" then
- loadAtlasWithNinePatch(stateComponent, stateComponent.atlas, "for state '" .. stateName .. "'")
- else
- -- Direct Image object (no ImageData available - scaleCorners won't work)
- stateComponent._loadedAtlas = stateComponent.atlas
- end
- end
-
- if stateComponent.insets then
- createRegionsFromInsets(stateComponent, component._loadedAtlas or self.atlas)
- end
- end
- end
- end
-
- -- Load scrollbar-specific atlases and process 9-patch definitions
- -- Scrollbars can have 'bar' and 'frame' subcomponents
- for scrollbarName, scrollbarDef in pairs(self.scrollbars) do
- -- Handle scrollbar definitions with bar/frame subcomponents
- if scrollbarDef.bar or scrollbarDef.frame then
- -- Process 'bar' subcomponent
- if scrollbarDef.bar then
- if type(scrollbarDef.bar) == "string" then
- -- Convert string path to ThemeComponent structure
- local barComponent = { atlas = scrollbarDef.bar }
- -- Copy knobOffset from parent scrollbarDef if it exists
- if scrollbarDef.knobOffset then
- barComponent.knobOffset = scrollbarDef.knobOffset
- end
- loadAtlasWithNinePatch(barComponent, scrollbarDef.bar, "for scrollbar '" .. scrollbarName .. ".bar'")
- if barComponent.insets then
- createRegionsFromInsets(barComponent, barComponent._loadedAtlas or self.atlas)
- end
- scrollbarDef.bar = barComponent
- elseif type(scrollbarDef.bar) == "table" then
- -- Already a ThemeComponent structure, process it
- -- Copy knobOffset from parent if bar component doesn't have one
- if scrollbarDef.knobOffset and not scrollbarDef.bar.knobOffset then
- scrollbarDef.bar.knobOffset = scrollbarDef.knobOffset
- end
- if scrollbarDef.bar.atlas and type(scrollbarDef.bar.atlas) == "string" then
- loadAtlasWithNinePatch(
- scrollbarDef.bar,
- scrollbarDef.bar.atlas,
- "for scrollbar '" .. scrollbarName .. ".bar'"
- )
- end
- if scrollbarDef.bar.insets then
- createRegionsFromInsets(scrollbarDef.bar, scrollbarDef.bar._loadedAtlas or self.atlas)
- end
- end
- end
-
- -- Process 'frame' subcomponent
- if scrollbarDef.frame then
- if type(scrollbarDef.frame) == "string" then
- -- Convert string path to ThemeComponent structure
- local frameComponent = { atlas = scrollbarDef.frame }
- loadAtlasWithNinePatch(frameComponent, scrollbarDef.frame, "for scrollbar '" .. scrollbarName .. ".frame'")
- if frameComponent.insets then
- createRegionsFromInsets(frameComponent, frameComponent._loadedAtlas or self.atlas)
- end
- scrollbarDef.frame = frameComponent
- elseif type(scrollbarDef.frame) == "table" then
- -- Already a ThemeComponent structure, process it
- if scrollbarDef.frame.atlas and type(scrollbarDef.frame.atlas) == "string" then
- loadAtlasWithNinePatch(
- scrollbarDef.frame,
- scrollbarDef.frame.atlas,
- "for scrollbar '" .. scrollbarName .. ".frame'"
- )
- end
- if scrollbarDef.frame.insets then
- createRegionsFromInsets(scrollbarDef.frame, scrollbarDef.frame._loadedAtlas or self.atlas)
- end
- end
- end
- else
- -- Treat as a single ThemeComponent (no bar/frame split)
- if scrollbarDef.atlas then
- if type(scrollbarDef.atlas) == "string" then
- loadAtlasWithNinePatch(scrollbarDef, scrollbarDef.atlas, "for scrollbar '" .. scrollbarName .. "'")
- else
- scrollbarDef._loadedAtlas = scrollbarDef.atlas
- end
- end
-
- if scrollbarDef.insets then
- createRegionsFromInsets(scrollbarDef, self.atlas)
- end
-
- if scrollbarDef.states then
- for stateName, stateComponent in pairs(scrollbarDef.states) do
- if stateComponent.atlas then
- if type(stateComponent.atlas) == "string" then
- loadAtlasWithNinePatch(
- stateComponent,
- stateComponent.atlas,
- "for scrollbar '" .. scrollbarName .. "' state '" .. stateName .. "'"
- )
- else
- stateComponent._loadedAtlas = stateComponent.atlas
- end
- end
-
- if stateComponent.insets then
- createRegionsFromInsets(stateComponent, scrollbarDef._loadedAtlas or self.atlas)
- end
- end
- end
- end
- end
-
- return self
-end
-
---- Import a theme definition from a file to enable hot-reloading and modular design systems
---- Use this to load bundled or user-created themes dynamically
----@param path string Path to theme definition file (e.g., "space" or "mytheme")
----@return Theme? theme The loaded theme, or nil on error
-function Theme.load(path)
- local definition
- local themePath = FLEXLOVE_BASE_PATH .. ".themes." .. path
-
- local success, result = pcall(function()
- return require(themePath)
- end)
- if success then
- definition = result
- else
- success, result = pcall(function()
- return require(path)
- end)
- if success then
- definition = result
- else
- Theme._ErrorHandler:warn("Theme", "RES_004", {
- theme = path,
- tried = themePath,
- error = tostring(result),
- fallback = "nil (no theme loaded)",
- })
- return nil
- end
- end
-
- local theme = Theme.new(definition)
- themes[theme.name] = theme
- themes[path] = theme
-
- return theme
-end
-
---- Switch the global theme to instantly restyle all themed UI elements
---- Use this to implement light/dark mode toggles or user-selectable skins
----@param themeOrName Theme|string Theme instance or theme name to activate
-function Theme.setActive(themeOrName)
- if type(themeOrName) == "string" then
- -- Try to load if not already loaded
- if not themes[themeOrName] then
- Theme.load(themeOrName)
- end
- activeTheme = themes[themeOrName]
- else
- activeTheme = themeOrName
- end
-
- if not activeTheme then
- Theme._ErrorHandler:warn("Theme", "THM_002", {
- theme = tostring(themeOrName),
- reason = "Theme not found or not loaded",
- fallback = "current theme unchanged",
- })
- -- Keep current activeTheme unchanged (fallback behavior)
- end
-end
-
---- Access the current theme to query colors, fonts, or create theme-aware components
---- Use this to build UI that adapts to the active design system
----@return Theme? theme The active theme, or nil if none is active
-function Theme.getActive()
- return activeTheme
-end
-
---- Retrieve pre-configured visual styles for UI components to maintain consistency
---- Use this to apply theme definitions to custom elements
----@param componentName string Name of the component (e.g., "button", "panel")
----@param state string? Optional state (e.g., "hover", "pressed", "disabled")
----@return ThemeComponent? component Returns component or nil if not found
-function Theme.getComponent(componentName, state)
- if not activeTheme then
- return nil
- end
-
- local component = activeTheme.components[componentName]
- if not component then
- return nil
- end
-
- -- Check for state-specific override
- if state and component.states and component.states[state] then
- return component.states[state]
- end
-
- return component
-end
-
---- Get the first (default) scrollbar from the active theme
---- Returns the first scrollbar component in insertion order
----@return ThemeComponent? scrollbar Returns first scrollbar component or nil if no scrollbars defined
-function Theme.getDefaultScrollbar()
- if not activeTheme or not activeTheme.scrollbars then
- return nil
- end
-
- local _, scrollbar = next(activeTheme.scrollbars)
- return scrollbar
-end
-
---- Retrieve themed scrollbar components for consistent scrollbar styling
---- Use this to apply theme-based scrollbar appearance to scrollable elements
----@param scrollbarName string? Name of the scrollbar style (e.g., "v1", "v2"). If nil, returns default (first) scrollbar
----@param state string? Optional state name (e.g., "hover", "pressed") - currently unused for scrollbars
----@return ThemeComponent? scrollbar Returns scrollbar component or nil if not found
-function Theme.getScrollbar(scrollbarName, state)
- if not activeTheme or not activeTheme.scrollbars then
- return nil
- end
-
- -- If no scrollbarName specified, return default (first) scrollbar
- if not scrollbarName then
- return Theme.getDefaultScrollbar()
- end
-
- local scrollbar = activeTheme.scrollbars[scrollbarName]
- if not scrollbar then
- return nil
- end
-
- -- Check for state-specific override (if scrollbar supports states in the future)
- if state and scrollbar.states and scrollbar.states[state] then
- return scrollbar.states[state]
- end
-
- return scrollbar
-end
-
---- Access theme-defined fonts for consistent typography across your UI
---- Use this to load fonts specified in your theme definition
----@param fontName string Name of the font family (e.g., "default", "heading")
----@return string? fontPath Returns font path or nil if not found
-function Theme.getFont(fontName)
- if not activeTheme then
- return nil
- end
-
- return activeTheme.fonts and activeTheme.fonts[fontName]
-end
-
---- Retrieve semantic colors from the theme palette for consistent brand identity
---- Use this instead of hardcoding colors to support themeing and color scheme switches
----@param colorName string Name of the color (e.g., "primary", "secondary")
----@return Color? color Returns Color instance or nil if not found
-function Theme.getColor(colorName)
- if not activeTheme then
- return nil
- end
-
- return activeTheme.colors and activeTheme.colors[colorName]
-end
-
---- Check if a theme is currently active
----@return boolean active Returns true if a theme is active
-function Theme.hasActive()
- return activeTheme ~= nil
-end
-
---- Get all registered theme names
----@return string[] themeNames Array of theme names
-function Theme.getRegisteredThemes()
- local themeNames = {}
- for name, _ in pairs(themes) do
- table.insert(themeNames, name)
- end
- return themeNames
-end
-
---- Get all available color names from the active theme
----@return string[]? colorNames Array of color names, or nil if no theme active
-function Theme.getColorNames()
- if not activeTheme or not activeTheme.colors then
- return nil
- end
-
- local colorNames = {}
- for name, _ in pairs(activeTheme.colors) do
- table.insert(colorNames, name)
- end
- return colorNames
-end
-
---- Get all colors from the active theme
----@return table? colors Table of all colors, or nil if no theme active
-function Theme.getAllColors()
- if not activeTheme then
- return nil
- end
-
- return activeTheme.colors
-end
-
---- Safely get theme colors with guaranteed fallbacks to prevent missing color errors
---- Use this when you need a color value no matter what
----@param colorName string Name of the color to retrieve
----@param fallback Color? Fallback color if not found (default: white)
----@return Color color The color or fallback (guaranteed non-nil)
-function Theme.getColorOrDefault(colorName, fallback)
- local color = Theme.getColor(colorName)
- if color then
- return color
- end
-
- return fallback or Theme._Color.new(1, 1, 1, 1)
-end
-
---- Get a theme by name
----@param themeName string Name of the theme
----@return Theme? theme Returns theme or nil if not found
-function Theme.get(themeName)
- return themes[themeName]
-end
-
---------------------------------------------------------------------------------
--- ThemeManager: Instance-level theme state management
---------------------------------------------------------------------------------
-
----@class ThemeManager
-local ThemeManager = {}
-ThemeManager.__index = ThemeManager
-
----Create a new ThemeManager instance
----@param config table Configuration options {theme: string?, themeComponent: string?, disabled: boolean?, active: boolean?, disableHighlight: boolean?, themeStateLock: boolean|string?, themeComponentDisabledStates: string[]?, scaleCorners: number?, scalingAlgorithm: string?}
----@return ThemeManager manager The new ThemeManager instance
-function ThemeManager.new(config)
- local self = setmetatable({}, ThemeManager)
-
- self.theme = config.theme
- self.themeComponent = config.themeComponent
- self.disabled = config.disabled or false
- self.active = config.active or false
- self.disableHighlight = config.disableHighlight
- self.themeStateLock = config.themeStateLock or false
- self.scaleCorners = config.scaleCorners
- self.scalingAlgorithm = config.scalingAlgorithm
-
- -- Normalize themeComponentDisabledStates to a lookup set for O(1) checks
- self.themeComponentDisabledStates = {}
- if config.themeComponentDisabledStates then
- for _, state in ipairs(config.themeComponentDisabledStates) do
- if type(state) == "string" then
- self.themeComponentDisabledStates[state] = true
- end
- end
- end
-
- -- Set initial state based on themeStateLock
- if self.themeStateLock == true or self.themeStateLock == "default" then
- self._themeState = "normal"
- elseif type(self.themeStateLock) == "string" then
- self._themeState = self.themeStateLock
- else
- self._themeState = "normal"
- end
-
- return self
-end
-
----Update the theme state based on element interaction state
----@param isHovered boolean Whether element is hovered
----@param isPressed boolean Whether element is pressed
----@param isFocused boolean Whether element is focused (keyboard focus)
----@param isDisabled boolean Whether element is disabled
----@return string state The new theme state ("normal", "hover", "pressed", "active", "disabled")
-function ThemeManager:updateState(isHovered, isPressed, isFocused, isDisabled)
- -- If themeStateLock is set (and not false), use the locked state
- if self.themeStateLock ~= false and self.themeStateLock ~= nil then
- local lockedState
-
- if self.themeStateLock == true or self.themeStateLock == "default" then
- -- true or "default" means lock to "normal" (base state)
- lockedState = "normal"
- elseif type(self.themeStateLock) == "string" then
- -- String means lock to specific state
- lockedState = self.themeStateLock
-
- -- Validate the locked state exists in the theme component (will be done during initialization)
- -- For now, just use the string value
- else
- -- Invalid themeStateLock value, fall back to normal behavior
- lockedState = nil
- end
-
- if lockedState then
- self._themeState = lockedState
- return lockedState
- end
- end
-
- -- Normal behavior: calculate state based on interaction
- -- Keyboard focus reuses the hover state so themes only need one visual variant.
- -- Priority: disabled > active > pressed > hover/focus > normal
- -- If a state is in themeComponentDisabledStates, fall through to the next lower-priority state.
- local candidates = {
- { state = "disabled", condition = isDisabled or self.disabled },
- { state = "active", condition = self.active },
- { state = "pressed", condition = isPressed },
- { state = "hover", condition = isHovered or isFocused },
- }
-
- local newState = "normal"
- for _, candidate in ipairs(candidates) do
- if candidate.condition and not self.themeComponentDisabledStates[candidate.state] then
- newState = candidate.state
- break
- end
- end
-
- self._themeState = newState
- return newState
-end
-
----Get the current theme state
----@return string state The current theme state
-function ThemeManager:getState()
- return self._themeState
-end
-
----Set the theme state explicitly
----@param state string The theme state to set ("normal", "hover", "pressed", "active", "disabled")
-function ThemeManager:setState(state)
- if type(state) ~= "string" then
- return
- end
- self._themeState = state
-end
-
----Check if a theme component is set
----@return boolean hasComponent True if a theme component is set
-function ThemeManager:hasThemeComponent()
- return self.themeComponent ~= nil
-end
-
----Get the theme (either instance-specific or active theme)
----@return Theme? theme The theme instance, or nil if not found
-function ThemeManager:getTheme()
- if self.theme then
- return Theme.get(self.theme)
- end
- return Theme.getActive()
-end
-
----Get the base theme component
----@return ThemeComponent? component The theme component, or nil if not found
-function ThemeManager:getComponent()
- if not self.themeComponent then
- return nil
- end
-
- local themeToUse = self:getTheme()
- if not themeToUse or not themeToUse.components or type(themeToUse.components) ~= "table" then
- return nil
- end
-
- if not themeToUse.components[self.themeComponent] then
- return nil
- end
-
- return themeToUse.components[self.themeComponent]
-end
-
----Get the theme component for the current state
----@return ThemeComponent? component The state-specific component, or base component, or nil
-function ThemeManager:getStateComponent()
- local component = self:getComponent()
- if not component then
- return nil
- end
-
- local state = self._themeState
- if
- state
- and state ~= "normal"
- and component.states
- and type(component.states) == "table"
- and component.states[state]
- then
- return component.states[state]
- end
-
- return component
-end
-
----Get a scrollbar component from the theme
----@param scrollbarName string? The scrollbar style name (e.g., "v1", "v2"). If nil, returns default (first) scrollbar
----@return ThemeComponent? scrollbar The scrollbar component, or nil if not found
-function ThemeManager:getScrollbarComponent(scrollbarName)
- local themeToUse = self:getTheme()
- if not themeToUse or not themeToUse.scrollbars or type(themeToUse.scrollbars) ~= "table" then
- return nil
- end
-
- if not scrollbarName then
- local _, scrollbar = next(themeToUse.scrollbars)
- return scrollbar
- end
-
- return themeToUse.scrollbars[scrollbarName]
-end
-
----Get a style property from the current state component
----@param property string The property name
----@return any? value The property value, or nil if not found
-function ThemeManager:getStyle(property)
- if type(property) ~= "string" then
- return nil
- end
-
- local stateComponent = self:getStateComponent()
- if not stateComponent or type(stateComponent) ~= "table" then
- return nil
- end
-
- return stateComponent[property]
-end
-
----Get scaled content padding based on border box dimensions
----@param borderBoxWidth number The border box width
----@param borderBoxHeight number The border box height
----@return table? padding Table with {left, top, right, bottom}, or nil if no contentPadding
-function ThemeManager:_getScaledContentPaddingForComponent(component, borderBoxWidth, borderBoxHeight)
- if not component or not component._ninePatchData or not component._ninePatchData.contentPadding then
- return nil
- end
-
- local contentPadding = component._ninePatchData.contentPadding
- local themeToUse = self:getTheme()
- local atlasImage = component._loadedAtlas or (themeToUse and themeToUse.atlas)
-
- if atlasImage and type(atlasImage) ~= "string" then
- local originalWidth, originalHeight = atlasImage:getDimensions()
-
- local insets = component.insets
- if insets and type(insets) == "table" then
- local cornerScale = self.scaleCorners
- if cornerScale == nil then
- cornerScale = component.scaleCorners
- end
- if type(cornerScale) ~= "number" or cornerScale <= 0 then
- cornerScale = 1
- end
-
- local function mapDistanceFromStart(sourceDistance, sourceSize, targetSize, sourceStartInset, sourceEndInset)
- local sourceStart = sourceStartInset or 0
- local sourceEnd = sourceEndInset or 0
-
- local sourceCenter = math.max(0, sourceSize - sourceStart - sourceEnd)
- local targetStart = sourceStart * cornerScale
- local targetEnd = sourceEnd * cornerScale
- local targetCenter = math.max(0, targetSize - targetStart - targetEnd)
-
- if sourceDistance <= sourceStart then
- return sourceDistance * cornerScale
- end
-
- if sourceDistance >= (sourceSize - sourceEnd) then
- local distanceFromEnd = sourceSize - sourceDistance
- return targetSize - (distanceFromEnd * cornerScale)
- end
-
- if sourceCenter <= 0 then
- return targetStart
- end
-
- local t = (sourceDistance - sourceStart) / sourceCenter
- return targetStart + (t * targetCenter)
- end
-
- local left = mapDistanceFromStart(contentPadding.left, originalWidth, borderBoxWidth, insets.left, insets.right)
- local rightBoundary = mapDistanceFromStart(
- originalWidth - contentPadding.right,
- originalWidth,
- borderBoxWidth,
- insets.left,
- insets.right
- )
- local right = borderBoxWidth - rightBoundary
-
- local top = mapDistanceFromStart(contentPadding.top, originalHeight, borderBoxHeight, insets.top, insets.bottom)
- local bottomBoundary = mapDistanceFromStart(
- originalHeight - contentPadding.bottom,
- originalHeight,
- borderBoxHeight,
- insets.top,
- insets.bottom
- )
- local bottom = borderBoxHeight - bottomBoundary
-
- return {
- left = math.max(0, left),
- top = math.max(0, top),
- right = math.max(0, right),
- bottom = math.max(0, bottom),
- }
- end
-
- local scaleX = borderBoxWidth / originalWidth
- local scaleY = borderBoxHeight / originalHeight
- return {
- left = contentPadding.left * scaleX,
- top = contentPadding.top * scaleY,
- right = contentPadding.right * scaleX,
- bottom = contentPadding.bottom * scaleY,
- }
- end
-
- return nil
-end
-
----Get scaled content padding for a specific theme state
----@param state string The theme state to resolve (e.g. "normal", "pressed")
----@param borderBoxWidth number The border box width
----@param borderBoxHeight number The border box height
----@return table? padding Table with {left, top, right, bottom}, or nil if no contentPadding
-function ThemeManager:_getScaledContentPaddingForState(state, borderBoxWidth, borderBoxHeight)
- if not self.themeComponent then
- return nil
- end
-
- local themeToUse = self:getTheme()
- if not themeToUse or not themeToUse.components[self.themeComponent] then
- return nil
- end
-
- local component = themeToUse.components[self.themeComponent]
-
- local stateToUse = state or "normal"
- if stateToUse ~= "normal" and component.states and component.states[stateToUse] then
- component = component.states[stateToUse]
- end
-
- return self:_getScaledContentPaddingForComponent(component, borderBoxWidth, borderBoxHeight)
-end
-
----@param state string The theme state to resolve (e.g. "normal", "pressed")
----@param borderBoxWidth number The border box width
----@param borderBoxHeight number The border box height
----@return table? padding Table with {left, top, right, bottom}, or nil if no contentPadding
-function ThemeManager:getScaledContentPaddingForState(state, borderBoxWidth, borderBoxHeight)
- Theme._ErrorHandler:warnDeprecated("Theme", "getScaledContentPaddingForState", "getScaledContentPadding")
- return self:getScaledContentPadding(borderBoxWidth, borderBoxHeight)
-end
-
----Get scaled content padding based on current theme state and border box dimensions
----@param borderBoxWidth number The border box width
----@param borderBoxHeight number The border box height
----@return table? padding Table with {left, top, right, bottom}, or nil if no contentPadding
-function ThemeManager:getScaledContentPadding(borderBoxWidth, borderBoxHeight)
- local state = self._themeState or "normal"
- return self:_getScaledContentPaddingForState(state, borderBoxWidth, borderBoxHeight)
-end
-
----Get content auto-sizing multiplier from theme or component
----@return table? multiplier Table with {width: number?, height: number?}, or nil if not defined
-function ThemeManager:getContentAutoSizingMultiplier()
- if not self.themeComponent then
- return nil
- end
-
- local themeToUse = self:getTheme()
- if not themeToUse then
- return nil
- end
-
- if self.themeComponent and themeToUse.components and type(themeToUse.components) == "table" then
- local component = themeToUse.components[self.themeComponent]
- if component and component.contentAutoSizingMultiplier then
- return component.contentAutoSizingMultiplier
- elseif themeToUse.contentAutoSizingMultiplier then
- return themeToUse.contentAutoSizingMultiplier
- end
- end
-
- if themeToUse.contentAutoSizingMultiplier then
- return themeToUse.contentAutoSizingMultiplier
- end
-
- return nil
-end
-
----Get the default font family path from the theme
----@return string? fontPath The default font path, or nil if not defined
-function ThemeManager:getDefaultFontFamily()
- local themeToUse = self:getTheme()
- if themeToUse and themeToUse.fonts and type(themeToUse.fonts) == "table" and themeToUse.fonts["default"] then
- return themeToUse.fonts["default"]
- end
- return nil
-end
-
----Set the theme and component for this ThemeManager
----@param themeName string? The theme name to use (nil to use active theme)
----@param componentName string? The component name to use
-function ThemeManager:setTheme(themeName, componentName)
- self.theme = themeName
- self.themeComponent = componentName
-end
-
----Validate themeStateLock and warn if invalid
----@return boolean isValid True if themeStateLock is valid or false/nil
-function ThemeManager:validateThemeStateLock()
- -- false or nil is always valid (no lock)
- if not self.themeStateLock or self.themeStateLock == false then
- return true
- end
-
- -- true is always valid (lock to normal)
- if self.themeStateLock == true then
- return true
- end
-
- -- String value needs validation
- if type(self.themeStateLock) == "string" then
- -- "default" is always valid (lock to normal/base state)
- if self.themeStateLock == "default" then
- return true
- end
-
- local component = self:getComponent()
-
- -- If no component, warn that themeStateLock has no effect
- if not component then
- if self.themeComponent then
- Theme._ErrorHandler:warn("Theme", "THM_007", {
- themeComponent = self.themeComponent,
- reason = "themeStateLock has no effect without a valid theme component",
- })
- end
- self.themeStateLock = false
- return false
- end
-
- -- Check if component has any states at all
- if not component.states or type(component.states) ~= "table" or next(component.states) == nil then
- Theme._ErrorHandler:warn("Theme", "THM_008", {
- themeComponent = self.themeComponent,
- reason = "Theme component has no state variants, themeStateLock has no effect",
- })
- self.themeStateLock = false
- return false
- end
-
- -- Check if the specified state exists
- if not component.states[self.themeStateLock] then
- -- Warn and fall back to false (no lock)
- Theme._ErrorHandler:warn("Theme", "THM_009", {
- themeComponent = self.themeComponent,
- requestedState = self.themeStateLock,
- availableStates = table.concat(self:_getAvailableStates(component), ", "),
- fallback = "themeStateLock disabled (using dynamic state)",
- })
- self.themeStateLock = false
- return false
- end
-
- return true
- end
-
- -- Invalid type for themeStateLock
- Theme._ErrorHandler:warn("Theme", "THM_010", {
- themeStateLockType = type(self.themeStateLock),
- reason = "themeStateLock must be boolean or string",
- fallback = "themeStateLock disabled",
- })
- self.themeStateLock = false
- return false
-end
-
----Get available state names for a component
----@param component ThemeComponent The component to check
----@return table stateNames Array of state names
-function ThemeManager:_getAvailableStates(component)
- local states = {}
- if component and component.states and type(component.states) == "table" then
- for stateName, _ in pairs(component.states) do
- table.insert(states, stateName)
- end
- end
- return states
-end
-
-Theme.Manager = ThemeManager
-
---- Check theme definitions for correctness before use to catch configuration errors early
---- Use this during development to verify custom themes are properly structured
----@param theme table? The theme to validate
----@param options table? Optional validation options {strict: boolean}
----@return boolean valid, table errors List of validation errors
-function Theme.validateTheme(theme, options)
- local errors = {}
- options = options or {}
-
- -- Basic structure validation
- if theme == nil then
- table.insert(errors, "Theme is nil")
- return false, errors
- end
-
- if type(theme) ~= "table" then
- table.insert(errors, "Theme must be a table")
- return false, errors
- end
-
- -- Name validation (only required field)
- if not theme.name then
- table.insert(errors, "Theme must have a 'name' field")
- elseif type(theme.name) ~= "string" then
- table.insert(errors, "Theme 'name' must be a string")
- elseif theme.name == "" then
- table.insert(errors, "Theme 'name' cannot be empty")
- end
-
- -- Colors validation (optional, but if present must be valid)
- if theme.colors ~= nil then
- if type(theme.colors) ~= "table" then
- table.insert(errors, "Theme 'colors' must be a table")
- else
- for colorName, colorValue in pairs(theme.colors) do
- if type(colorName) ~= "string" then
- table.insert(errors, "Color name must be a string, got " .. type(colorName))
- else
- -- Accept Color objects, hex strings, or named colors
- local colorType = type(colorValue)
- if colorType == "table" then
- -- Assume it's a Color object if it has r,g,b fields
- if not (colorValue.r and colorValue.g and colorValue.b) then
- table.insert(errors, "Color '" .. colorName .. "' is not a valid Color object")
- end
- elseif colorType == "string" then
- -- Validate color string
- local isValid, err = Theme._Color.validateColor(colorValue)
- if not isValid then
- table.insert(errors, "Color '" .. colorName .. "': " .. err)
- end
- else
- table.insert(errors, "Color '" .. colorName .. "' must be a Color object or string")
- end
- end
- end
- end
- end
-
- -- Fonts validation (optional)
- if theme.fonts ~= nil then
- if type(theme.fonts) ~= "table" then
- table.insert(errors, "Theme 'fonts' must be a table")
- else
- for fontName, fontPath in pairs(theme.fonts) do
- if type(fontName) ~= "string" then
- table.insert(errors, "Font name must be a string, got " .. type(fontName))
- elseif type(fontPath) ~= "string" then
- table.insert(errors, "Font '" .. fontName .. "' path must be a string")
- end
- end
- end
- end
-
- -- Components validation (optional)
- if theme.components ~= nil then
- if type(theme.components) ~= "table" then
- table.insert(errors, "Theme 'components' must be a table")
- else
- for componentName, component in pairs(theme.components) do
- if type(component) == "table" then
- -- Validate atlas if present
- if component.atlas ~= nil and type(component.atlas) ~= "string" then
- table.insert(errors, "Component '" .. componentName .. "' atlas must be a string")
- end
-
- -- Validate insets if present
- if component.insets ~= nil then
- if type(component.insets) ~= "table" then
- table.insert(errors, "Component '" .. componentName .. "' insets must be a table")
- else
- -- If insets are provided, all 4 sides must be present
- for _, side in ipairs({ "left", "top", "right", "bottom" }) do
- if component.insets[side] == nil then
- table.insert(errors, "Component '" .. componentName .. "' insets must have '" .. side .. "' field")
- elseif type(component.insets[side]) ~= "number" then
- table.insert(errors, "Component '" .. componentName .. "' insets." .. side .. " must be a number")
- elseif component.insets[side] < 0 then
- table.insert(errors, "Component '" .. componentName .. "' insets." .. side .. " must be non-negative")
- end
- end
- end
- end
-
- -- Validate states if present
- if component.states ~= nil then
- if type(component.states) ~= "table" then
- table.insert(errors, "Component '" .. componentName .. "' states must be a table")
- else
- for stateName, stateComponent in pairs(component.states) do
- if type(stateComponent) ~= "table" then
- table.insert(
- errors,
- "Component '" .. componentName .. "' state '" .. stateName .. "' must be a table"
- )
- end
- end
- end
- end
-
- -- Validate scaleCorners if present
- if component.scaleCorners ~= nil then
- if type(component.scaleCorners) ~= "number" then
- table.insert(errors, "Component '" .. componentName .. "' scaleCorners must be a number")
- elseif component.scaleCorners <= 0 then
- table.insert(errors, "Component '" .. componentName .. "' scaleCorners must be positive")
- end
- end
-
- -- Validate scalingAlgorithm if present
- if component.scalingAlgorithm ~= nil then
- if type(component.scalingAlgorithm) ~= "string" then
- table.insert(errors, "Component '" .. componentName .. "' scalingAlgorithm must be a string")
- elseif component.scalingAlgorithm ~= "nearest" and component.scalingAlgorithm ~= "bilinear" then
- table.insert(
- errors,
- "Component '" .. componentName .. "' scalingAlgorithm must be 'nearest' or 'bilinear'"
- )
- end
- end
- end
- end
- end
- end
-
- -- Scrollbars validation (optional)
- if theme.scrollbars ~= nil then
- if type(theme.scrollbars) ~= "table" then
- table.insert(errors, "Theme 'scrollbars' must be a table")
- else
- for scrollbarName, scrollbarDef in pairs(theme.scrollbars) do
- if type(scrollbarDef) == "table" then
- -- Check if it has bar/frame subcomponents
- if scrollbarDef.bar or scrollbarDef.frame then
- -- Validate bar subcomponent
- if scrollbarDef.bar ~= nil then
- if type(scrollbarDef.bar) ~= "string" and type(scrollbarDef.bar) ~= "table" then
- table.insert(errors, "Scrollbar '" .. scrollbarName .. "' bar must be a string or table")
- end
- end
- -- Validate frame subcomponent
- if scrollbarDef.frame ~= nil then
- if type(scrollbarDef.frame) ~= "string" and type(scrollbarDef.frame) ~= "table" then
- table.insert(errors, "Scrollbar '" .. scrollbarName .. "' frame must be a string or table")
- end
- end
- else
- -- Validate as a single ThemeComponent
- -- Validate atlas if present
- if scrollbarDef.atlas ~= nil and type(scrollbarDef.atlas) ~= "string" then
- table.insert(errors, "Scrollbar '" .. scrollbarName .. "' atlas must be a string")
- end
-
- -- Validate insets if present
- if scrollbarDef.insets ~= nil then
- if type(scrollbarDef.insets) ~= "table" then
- table.insert(errors, "Scrollbar '" .. scrollbarName .. "' insets must be a table")
- else
- for _, side in ipairs({ "left", "top", "right", "bottom" }) do
- if scrollbarDef.insets[side] == nil then
- table.insert(errors, "Scrollbar '" .. scrollbarName .. "' insets must have '" .. side .. "' field")
- elseif type(scrollbarDef.insets[side]) ~= "number" then
- table.insert(errors, "Scrollbar '" .. scrollbarName .. "' insets." .. side .. " must be a number")
- elseif scrollbarDef.insets[side] < 0 then
- table.insert(
- errors,
- "Scrollbar '" .. scrollbarName .. "' insets." .. side .. " must be non-negative"
- )
- end
- end
- end
- end
-
- -- Validate states if present
- if scrollbarDef.states ~= nil then
- if type(scrollbarDef.states) ~= "table" then
- table.insert(errors, "Scrollbar '" .. scrollbarName .. "' states must be a table")
- else
- for stateName, stateComponent in pairs(scrollbarDef.states) do
- if type(stateComponent) ~= "table" then
- table.insert(
- errors,
- "Scrollbar '" .. scrollbarName .. "' state '" .. stateName .. "' must be a table"
- )
- end
- end
- end
- end
- end
- end
- end
- end
- end
-
- -- contentAutoSizingMultiplier validation (optional)
- if theme.contentAutoSizingMultiplier ~= nil then
- if type(theme.contentAutoSizingMultiplier) ~= "table" then
- table.insert(errors, "Theme 'contentAutoSizingMultiplier' must be a table")
- else
- if theme.contentAutoSizingMultiplier.width ~= nil then
- if type(theme.contentAutoSizingMultiplier.width) ~= "number" then
- table.insert(errors, "contentAutoSizingMultiplier.width must be a number")
- elseif theme.contentAutoSizingMultiplier.width <= 0 then
- table.insert(errors, "contentAutoSizingMultiplier.width must be positive")
- end
- end
- if theme.contentAutoSizingMultiplier.height ~= nil then
- if type(theme.contentAutoSizingMultiplier.height) ~= "number" then
- table.insert(errors, "contentAutoSizingMultiplier.height must be a number")
- elseif theme.contentAutoSizingMultiplier.height <= 0 then
- table.insert(errors, "contentAutoSizingMultiplier.height must be positive")
- end
- end
- end
- end
-
- -- Global atlas validation (optional)
- if theme.atlas ~= nil then
- if type(theme.atlas) ~= "string" then
- table.insert(errors, "Theme 'atlas' must be a string")
- end
- end
-
- -- Strict mode: warn about unknown fields
- if options.strict then
- local knownFields = {
- name = true,
- atlas = true,
- components = true,
- scrollbars = true,
- colors = true,
- fonts = true,
- contentAutoSizingMultiplier = true,
- }
- for field in pairs(theme) do
- if not knownFields[field] then
- table.insert(errors, "Unknown field '" .. field .. "' in theme")
- end
- end
- end
-
- return #errors == 0, errors
-end
-
---- Clean up malformed theme data to make it usable without crashing
---- Use this to robustly handle user-created or external themes
----@param theme table? The theme to sanitize
----@return table sanitized The sanitized theme
-function Theme.sanitizeTheme(theme)
- local sanitized = {}
-
- -- Handle nil theme
- if theme == nil then
- return { name = "Invalid Theme" }
- end
-
- -- Handle non-table theme
- if type(theme) ~= "table" then
- return { name = "Invalid Theme" }
- end
-
- -- Sanitize name
- if type(theme.name) == "string" and theme.name ~= "" then
- sanitized.name = theme.name
- else
- sanitized.name = "Unnamed Theme"
- end
-
- -- Sanitize colors
- if type(theme.colors) == "table" then
- sanitized.colors = {}
- for colorName, colorValue in pairs(theme.colors) do
- if type(colorName) == "string" then
- local colorType = type(colorValue)
- if colorType == "table" and colorValue.r and colorValue.g and colorValue.b then
- -- Valid Color object
- sanitized.colors[colorName] = colorValue
- elseif colorType == "string" then
- -- Try to validate color string
- local isValid = Theme._Color.validateColor(colorValue)
- if isValid then
- sanitized.colors[colorName] = colorValue
- else
- -- Provide fallback color
- sanitized.colors[colorName] = Theme._Color.new(0, 0, 0, 1)
- end
- end
- end
- end
- end
-
- -- Sanitize fonts
- if type(theme.fonts) == "table" then
- sanitized.fonts = {}
- for fontName, fontPath in pairs(theme.fonts) do
- if type(fontName) == "string" and type(fontPath) == "string" then
- sanitized.fonts[fontName] = fontPath
- end
- end
- end
-
- -- Sanitize components (preserve as-is, they're complex)
- if type(theme.components) == "table" then
- sanitized.components = theme.components
- end
-
- -- Sanitize scrollbars (preserve as-is, they're complex like components)
- if type(theme.scrollbars) == "table" then
- sanitized.scrollbars = theme.scrollbars
- end
-
- -- Sanitize contentAutoSizingMultiplier
- if type(theme.contentAutoSizingMultiplier) == "table" then
- sanitized.contentAutoSizingMultiplier = {}
- if type(theme.contentAutoSizingMultiplier.width) == "number" and theme.contentAutoSizingMultiplier.width > 0 then
- sanitized.contentAutoSizingMultiplier.width = theme.contentAutoSizingMultiplier.width
- end
- if type(theme.contentAutoSizingMultiplier.height) == "number" and theme.contentAutoSizingMultiplier.height > 0 then
- sanitized.contentAutoSizingMultiplier.height = theme.contentAutoSizingMultiplier.height
- end
- end
-
- -- Sanitize atlas
- if type(theme.atlas) == "string" then
- sanitized.atlas = theme.atlas
- end
-
- return sanitized
-end
-
-return Theme
diff --git a/libs/flexlove/modules/UTF8.lua b/libs/flexlove/modules/UTF8.lua
deleted file mode 100644
index ba2adc95..00000000
--- a/libs/flexlove/modules/UTF8.lua
+++ /dev/null
@@ -1,44 +0,0 @@
----@class UTF8
----Compatibility layer for UTF-8 support across Lua versions
----Handles utf8 (Lua 5.3+), lua-utf8 (LuaRocks), and basic fallbacks
-
-local UTF8 = {}
-
--- Try to load UTF-8 library in order of preference:
--- 1. Built-in utf8 (Lua 5.3+, LÖVE2D)
--- 2. lua-utf8 from LuaRocks (Lua 5.1, 5.2)
--- 3. Error if neither available
-local function loadUTF8()
- -- Try built-in utf8 first (Lua 5.3+ and LÖVE2D)
- if utf8 and type(utf8) == "table" and utf8.len then
- return utf8
- end
-
- -- Try lua-utf8 from LuaRocks
- local ok, luautf8 = pcall(require, "lua-utf8")
- if ok then
- return luautf8
- end
-
- -- Try standard utf8 module name as fallback
- ok, luautf8 = pcall(require, "utf8")
- if ok then
- return luautf8
- end
-
- -- No UTF-8 library available
- error("No UTF-8 library available. Please install 'luautf8' via LuaRocks: luarocks install luautf8")
-end
-
--- Load the UTF-8 implementation
-local utf8lib = loadUTF8()
-
--- Export all utf8 functions
-UTF8.char = utf8lib.char
-UTF8.charpattern = utf8lib.charpattern
-UTF8.codes = utf8lib.codes
-UTF8.codepoint = utf8lib.codepoint
-UTF8.len = utf8lib.len
-UTF8.offset = utf8lib.offset
-
-return UTF8
diff --git a/libs/flexlove/modules/Units.lua b/libs/flexlove/modules/Units.lua
deleted file mode 100644
index aa25707f..00000000
--- a/libs/flexlove/modules/Units.lua
+++ /dev/null
@@ -1,335 +0,0 @@
---- Utility module for parsing and resolving CSS-like units (px, %, vw, vh)
---- Provides unit parsing, validation, and conversion to pixel values
----@class Units
----@field _Context table? Context module dependency
----@field _ErrorHandler table? ErrorHandler module dependency
----@field _Calc table? Calc module dependency
-local Units = {}
-
---- Initialize Units module with dependencies
----@param deps table Dependencies: { Context = table?, ErrorHandler = table?, Calc = table? }
-function Units.init(deps)
- Units._Context = deps.Context
- Units._ErrorHandler = deps.ErrorHandler
- Units._Calc = deps.Calc
-end
-
---- Parse a unit value into numeric value and unit type
---- Supports: px (pixels), % (percentage), vw/vh (viewport), and calc() expressions
----@param value string|number|table The value to parse (e.g., "50px", "10%", "2vw", 100, or calc object)
----@return number|table numericValue The numeric portion of the value or calc object
----@return string unitType The unit type ("px", "%", "vw", "vh", "calc")
-function Units.parse(value)
- -- Check if value is a calc expression
- if Units._Calc and Units._Calc.isCalc(value) then
- return value, "calc"
- end
-
- if type(value) == "number" then
- return value, "px"
- end
-
- if type(value) ~= "string" and type(value) ~= "table" then
- Units._ErrorHandler:warn("Units", "VAL_001", {
- property = "unit value",
- expected = "string, number, or calc object",
- got = type(value),
- })
- return 0, "px"
- end
-
- -- Check for unit-only input (e.g., "px", "%", "vw" without a number)
- local validUnits = { px = true, ["%"] = true, vw = true, vh = true }
- if validUnits[value] then
- Units._ErrorHandler:warn("Units", "VAL_005", {
- input = value,
- expected = "number + unit (e.g., '50" .. value .. "')",
- })
- return 0, "px"
- end
-
- -- Check for invalid format (space between number and unit)
- if value:match("%d%s+%a") then
- Units._ErrorHandler:warn("Units", "VAL_005", {
- input = value,
- issue = "contains space between number and unit",
- })
- return 0, "px"
- end
-
- -- Match number followed by optional unit
- local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
- if not numStr then
- Units._ErrorHandler:warn("Units", "VAL_005", {
- input = value,
- })
- return 0, "px"
- end
-
- local num = tonumber(numStr)
- if not num then
- Units._ErrorHandler:warn("Units", "VAL_005", {
- input = value,
- issue = "numeric value cannot be parsed",
- })
- return 0, "px"
- end
-
- -- Default to pixels if no unit specified
- if unit == "" then
- unit = "px"
- end
-
- -- validUnits is already defined at the top of the function
- if not validUnits[unit] then
- Units._ErrorHandler:warn("Units", "VAL_005", {
- input = value,
- unit = unit,
- validUnits = "px, %, vw, vh",
- })
- return num, "px"
- end
-
- return num, unit
-end
-
---- Convert relative units to absolute pixel values
---- Resolves %, vw, vh units based on viewport and parent dimensions, and evaluates calc() expressions
----@param value number|table Numeric value to convert or calc object
----@param unit string Unit type ("px", "%", "vw", "vh", "calc")
----@param viewportWidth number Current viewport width in pixels
----@param viewportHeight number Current viewport height in pixels
----@param parentSize number? Required for percentage units (parent dimension in pixels)
----@return number resolvedValue Resolved pixel value
-function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
- if unit == "calc" then
- -- Resolve calc expression
- if Units._Calc then
- return Units._Calc.resolve(value, viewportWidth, viewportHeight, parentSize)
- else
- Units._ErrorHandler:warn("Units", "VAL_006", {
- unit = "calc",
- issue = "Calc module not available",
- })
- return 0
- end
- elseif unit == "px" then
- return value
- elseif unit == "%" then
- if not parentSize then
- Units._ErrorHandler:warn("Units", "LAY_003", {
- unit = "%",
- issue = "parent dimension not available",
- })
- return 0
- end
- return (value / 100) * parentSize
- elseif unit == "vw" then
- return (value / 100) * viewportWidth
- elseif unit == "vh" then
- return (value / 100) * viewportHeight
- else
- Units._ErrorHandler:warn("Units", "VAL_005", {
- unit = unit,
- validUnits = "px, %, vw, vh, calc",
- })
- return 0
- end
-end
-
---- Get current viewport dimensions
---- Uses cached viewport during resize operations, otherwise queries LÖVE graphics
----@return number width Viewport width in pixels
----@return number height Viewport height in pixels
-function Units.getViewport()
- -- Return cached viewport if available (only during resize operations)
- if Units._Context._cachedViewport and Units._Context._cachedViewport.width > 0 then
- return Units._Context._cachedViewport.width, Units._Context._cachedViewport.height
- end
-
- if love.graphics and love.graphics.getDimensions then
- return love.graphics.getDimensions()
- else
- local w, h = love.window.getMode()
- return w, h
- end
-end
-
---- Apply base scale factor to a value based on axis
---- Used for responsive scaling of UI elements
----@param value number The value to scale
----@param axis "x"|"y" The axis to scale on
----@param scaleFactors {x:number, y:number} Scale factors for each axis
----@return number scaledValue The scaled value
-function Units.applyBaseScale(value, axis, scaleFactors)
- if axis == "x" then
- return value * scaleFactors.x
- else
- return value * scaleFactors.y
- end
-end
-
---- Resolve spacing properties (margin, padding) to pixel values
---- Supports individual sides (top, right, bottom, left) and shortcuts (vertical, horizontal)
----@param spacingProps table? Spacing properties with top/right/bottom/left/vertical/horizontal
----@param parentWidth number Parent element width in pixels
----@param parentHeight number Parent element height in pixels
----@return table resolvedSpacing Table with top, right, bottom, left in pixels
-function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
- if not spacingProps then
- return { top = 0, right = 0, bottom = 0, left = 0 }
- end
-
- local viewportWidth, viewportHeight = Units.getViewport()
- local result = {}
-
- local vertical = spacingProps.vertical
- local horizontal = spacingProps.horizontal
-
- if vertical then
- if type(vertical) == "string" or (Units._Calc and Units._Calc.isCalc(vertical)) then
- local value, unit = Units.parse(vertical)
- vertical = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
- end
- end
-
- if horizontal then
- if type(horizontal) == "string" or (Units._Calc and Units._Calc.isCalc(horizontal)) then
- local value, unit = Units.parse(horizontal)
- horizontal = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
- end
- end
-
- for _, side in ipairs({ "top", "right", "bottom", "left" }) do
- local value = spacingProps[side]
- if value then
- if type(value) == "string" or (Units._Calc and Units._Calc.isCalc(value)) then
- local numValue, unit = Units.parse(value)
- local parentSize = (side == "top" or side == "bottom") and parentHeight or parentWidth
- result[side] = Units.resolve(numValue, unit, viewportWidth, viewportHeight, parentSize)
- else
- result[side] = value
- end
- else
- if side == "top" or side == "bottom" then
- result[side] = vertical or 0
- else
- result[side] = horizontal or 0
- end
- end
- end
-
- return result
-end
-
---- Validate a unit string format
---- Checks if the string can be successfully parsed as a valid unit or calc expression
----@param unitStr string|table The unit string to validate (e.g., "50px", "10%") or calc object
----@return boolean isValid True if the unit string is valid, false otherwise
-function Units.isValid(unitStr)
- -- Check if it's a calc expression
- if Units._Calc and Units._Calc.isCalc(unitStr) then
- return true
- end
-
- if type(unitStr) ~= "string" then
- return false
- end
-
- -- Check for invalid format (space between number and unit)
- if unitStr:match("%d%s+%a") then
- return false
- end
-
- -- Match number followed by optional unit
- local numStr, unit = unitStr:match("^([%-]?[%d%.]+)(.*)$")
- if not numStr then
- return false
- end
-
- -- Check if numeric part is valid
- local num = tonumber(numStr)
- if not num then
- return false
- end
-
- -- Default to pixels if no unit specified
- if unit == "" then
- unit = "px"
- end
-
- -- Check if unit is valid
- local validUnits = { px = true, ["%"] = true, vw = true, vh = true }
- return validUnits[unit] == true
-end
-
---- Parse CSS flex shorthand into flexGrow, flexShrink, flexBasis
---- Supports: number, "auto", "none", "grow shrink basis"
----@param flexValue number|string The flex shorthand value
----@return number flexGrow
----@return number flexShrink
----@return string|number flexBasis
-function Units.parseFlexShorthand(flexValue)
- -- Single number: flex-grow
- if type(flexValue) == "number" then
- return flexValue, 1, 0
- end
-
- -- String values
- if type(flexValue) == "string" then
- -- "auto" = 1 1 auto
- if flexValue == "auto" then
- return 1, 1, "auto"
- end
-
- -- "none" = 0 0 auto
- if flexValue == "none" then
- return 0, 0, "auto"
- end
-
- -- Parse "grow shrink basis" format
- local parts = {}
- for part in flexValue:gmatch("%S+") do
- table.insert(parts, part)
- end
-
- local grow = 0
- local shrink = 1
- local basis = "auto"
-
- if #parts == 1 then
- -- Single value: could be grow (number) or basis (with unit)
- local num = tonumber(parts[1])
- if num then
- grow = num
- basis = 0
- else
- basis = parts[1]
- end
- elseif #parts == 2 then
- -- Two values: grow shrink (both numbers) or grow basis
- local num1 = tonumber(parts[1])
- local num2 = tonumber(parts[2])
- if num1 and num2 then
- grow = num1
- shrink = num2
- basis = 0
- elseif num1 then
- grow = num1
- basis = parts[2]
- end
- elseif #parts >= 3 then
- -- Three values: grow shrink basis
- grow = tonumber(parts[1]) or 0
- shrink = tonumber(parts[2]) or 1
- basis = parts[3]
- end
-
- return grow, shrink, basis
- end
-
- -- Default fallback
- return 0, 1, "auto"
-end
-
-return Units
diff --git a/libs/flexlove/modules/ZIndex.lua b/libs/flexlove/modules/ZIndex.lua
deleted file mode 100644
index e10bdf56..00000000
--- a/libs/flexlove/modules/ZIndex.lua
+++ /dev/null
@@ -1,35 +0,0 @@
----@class ZIndex
-local ZIndex = {}
-
--- The effective z-index formula used for sorting is:
--- rootZ * ROOT_WEIGHT + depth * DEPTH_WEIGHT + ownZ
--- where rootZ is the z-index of the top-level ancestor, depth is the
--- nesting level, and ownZ is the element's own z property.
---
--- Constraints enforced by these weights:
--- |ownZ| <= MAX_Z (must fit within DEPTH_WEIGHT digits)
--- DEPTH_WEIGHT has enough room for depths well beyond any practical tree
--- ROOT_WEIGHT has enough room for the rootZ without exceeding double-precision
----
----@type integer
-ZIndex.MIN_Z = -999
----@type integer
-ZIndex.MAX_Z = 999
----@type integer
-ZIndex.ROOT_WEIGHT = 10000000000
----@type integer
-ZIndex.DEPTH_WEIGHT = 1000
-
---- Clamp a z-index value to the valid range
----@param value number
----@return integer
-function ZIndex.clamp(value)
- if value < ZIndex.MIN_Z then
- return ZIndex.MIN_Z
- elseif value > ZIndex.MAX_Z then
- return ZIndex.MAX_Z
- end
- return value
-end
-
-return ZIndex
diff --git a/libs/flexlove/modules/behaviors/Animated.lua b/libs/flexlove/modules/behaviors/Animated.lua
deleted file mode 100644
index 16592025..00000000
--- a/libs/flexlove/modules/behaviors/Animated.lua
+++ /dev/null
@@ -1,245 +0,0 @@
--- modules/behaviors/Animated.lua
---
--- Concrete behavior: animation update, interpolation application, chaining
--- resolution, and transition wiring.
---
--- Task 06 of the behavior-mode-unification refactor. Moves the entire
--- animation-update block out of Element:update (lines ~2761-2800) into
--- `Animated.onUpdate(element, dt)`, and the `_ColorModule`/`_TransformModule`
--- init-time wiring into `Animated.onAttach(element)`.
---
--- This behavior is UNIQUE among the behavior set because it can attach
--- AFTER element creation. Animation is opt-in: a plain Element created without
--- `transitions` and without an `animation` field never attaches Animated.
--- The moment something creates an animation on the element — either directly
--- (`element.animation = Animation.new(...)`, `element:fadeIn(...)`) or via a
--- transition firing in `setProperty` — `Animated.ensureAttached(element)`
--- attaches this behavior on demand so subsequent `Element:update` frames
--- dispatch to `Animated.onUpdate`.
---
--- Attachment rule (shouldAttach): true when `props.transitions` is set OR an
--- `element.animation` already exists at runtime. The runtime arm covers the
--- late-attach case (animateTo / fadeIn / direct animation assignment).
---
--- State ownership (per the locked Behavior contract):
--- * Per-element runtime state lives ON THE ELEMENT (`element.animation`).
--- * The behavior instance itself is stateless and shared across elements.
--- * Element-class-level dependencies (Element._Animation, Element._Color,
--- Element._Transform) are resolved from the owning element's metatable,
--- exactly like Clickable does — keeping the behavior stateless without
--- expanding the 6-hook signature.
---
--- saveState/restoreState are no-ops: animations are ephemeral (an in-flight
--- animation is not part of immediate-mode persisted state — the next frame
--- re-evaluates transitions / re-applies animations fresh). Persisted scalar
--- props (`opacity`, `x`, ...) survive via Element.saveState's `_props` block,
--- not via the animation.
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Resolve the Element class from an element instance.
--- Element instances are created via `setmetatable({}, Element)` in _construct,
--- so their metatable IS the Element class — giving us Element._Animation,
--- Element._Color, Element._Transform, etc. without threading deps through the
--- behavior hook signature.
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ----------------------------------------------------------------------------
--- ensureAnimationModuleWiring — set Element._Animation._ColorModule /
--- _TransformModule. Idempotent; called from both onAttach and onUpdate so it
--- works even when an animation was assigned by a caller that bypassed
--- onAttach (direct `element.animation = Animation.new(...)`).
--- ----------------------------------------------------------------------------
-
-local function ensureAnimationModuleWiring(element)
- local Element = ElementClass(element)
- local Animation = Element._Animation
- if not Animation then
- return
- end
- -- Ensure animation has Color module reference for color interpolation
- if not Animation._ColorModule and Element._Color then
- Animation._ColorModule = Element._Color
- end
- -- Ensure animation has Transform module reference for transform interpolation
- if not Animation._TransformModule and Element._Transform then
- Animation._TransformModule = Element._Transform
- end
-end
-
--- ----------------------------------------------------------------------------
--- shouldAttach (class-level predicate, no element required)
--- ----------------------------------------------------------------------------
-
--- True when the element declares transitions up front OR already has an
--- animation attached. The `animation` arm is consulted by ensureAttached at
--- runtime (after creation); the `transitions` arm lets Animated auto-attach
--- during Element.new for elements that pre-declare transitions.
-local function shouldAttach(props)
- if not props then
- return false
- end
- if props.transitions ~= nil then
- return true
- end
- -- Late-attach case: an animation was assigned after creation. When ensure
- -- Attached passes the element instance as `props`, this arm catches it.
- if type(props) == "table" and props.animation ~= nil then
- return true
- end
- return false
-end
-
--- ----------------------------------------------------------------------------
--- ensureAttached — dynamic late-attach entry point
--- ----------------------------------------------------------------------------
-
--- Idempotently attach the Animated behavior to an element that just gained an
--- animation (via animateTo / fadeIn / direct assignment / a firing transition
--- in setProperty). Called from Element.setProperty when a transition fires and
--- from the transition helper methods on Element. Safe to call when already
--- attached (no-op / returns false).
---
--- `animatedBehavior` is the shared behavior instance resolved lazily by
--- Element (see Element._resolveAnimatedBehavior). The behavior is looked up
--- from the registry once and cached on the class.
---
--- Returns true if the behavior was attached this call, false otherwise.
-local function ensureAttached(element, animatedBehavior)
- if not element or not animatedBehavior then
- return false
- end
- -- Already attached? Avoid duplicate entries within one element lifetime
- -- (a behavior may legitimately be re-added across immediate-mode frames
- -- since Element is recreated each frame, but within one lifetime at most
- -- once).
- local behaviors = element.behaviors
- if behaviors then
- for i = 1, #behaviors do
- if behaviors[i] == animatedBehavior then
- return false
- end
- end
- end
- table.insert(element.behaviors, animatedBehavior)
- animatedBehavior.onAttach(element)
- return true
-end
-
--- ----------------------------------------------------------------------------
--- onAttach — initialize Animation module references (formerly the
--- Element._Animation._ColorModule / _TransformModule wiring in Element:update
--- lines ~2772-2778).
--- ----------------------------------------------------------------------------
-
-local function onAttach(element)
- ensureAnimationModuleWiring(element)
-end
-
--- ----------------------------------------------------------------------------
--- onUpdate — the animation update + interpolation + chain-resolution block
--- (formerly Element:update lines ~2761-2800).
--- ----------------------------------------------------------------------------
-
-local function onUpdate(element, dt)
- local animation = element.animation
- if not animation then
- return
- end
-
- -- (Re)ensure module wiring is present in case the Animation instance was
- -- created by a caller that bypassed onAttach (e.g. direct
- -- `element.animation = Animation.new(...)`). Cheap idempotent writes.
- ensureAnimationModuleWiring(element)
-
- local finished = animation:update(dt, element)
- if finished then
- -- Animation:update() already called onComplete callback.
- -- Check for chained animation.
- if animation._next then
- element.animation = animation._next
- elseif animation._nextFactory and type(animation._nextFactory) == "function" then
- local success, nextAnim = pcall(animation._nextFactory, element)
- if success and nextAnim then
- element.animation = nextAnim
- else
- element.animation = nil
- end
- else
- element.animation = nil
- end
- else
- -- Apply animation interpolation during update.
- animation:applyInterpolation(element)
- end
-end
-
--- ----------------------------------------------------------------------------
--- saveState / restoreState — no-ops (animations are ephemeral).
--- ----------------------------------------------------------------------------
-
--- Animations are not persisted across immediate-mode frames — they are
--- re-derived each frame from transitions / direct calls. The element's scalar
--- props (opacity, x, ...) are persisted by Element.saveState's _props block,
--- so a completed animation's final visual state still survives recreation.
--- While an animation is mid-flight in immediate mode, the element is recreated
--- and the animation is NOT carried over (intentional — animating in immediate
--- mode requires setting up the animation each frame).
-local function saveState()
- return nil
-end
-
-local function restoreState()
- return nil
-end
-
--- ----------------------------------------------------------------------------
--- Build the (stateless, shared) behavior instance.
--- ----------------------------------------------------------------------------
-
--- onDetach/onDraw omitted: they default to no-ops (the behavior allocates no
--- behavior-local state and animations have no draw pass). Animation state lives
--- on the element (`element.animation`); nothing to tear down on detach.
---
--- We build the immutable behavior via Behavior.new (for validation + freeze +
--- isBehavior parity with Clickable), then expose the late-attach helper on a
--- thin module table since the frozen instance cannot accept new keys. The
--- module table passes the behavior to the registry while making
--- `Animated.ensureAttached` callable from Element.setProperty / the transition
--- helpers — exactly as the task spec requires.
-local behavior = Behavior.new({
- onAttach = onAttach,
- onUpdate = onUpdate,
- saveState = saveState,
- restoreState = restoreState,
- shouldAttach = shouldAttach,
-})
-
--- Thin module table: exposes the behavior instance (for the registry) plus the
--- late-attach helper (for Element.setProperty). All hooks delegate to the
--- frozen behavior instance so dispatch sites get the validated, frozen
--- implementation. shouldAttach is also exposed at module level (mirrors
--- Clickable.shouldAttach) for tests/callers without an element.
-local Animated = {
- behavior = behavior,
- ensureAttached = ensureAttached,
- shouldAttach = shouldAttach,
- onAttach = onAttach,
- onUpdate = onUpdate,
-}
-
--- Metatable so the module table itself satisfies the duck-typed registry
--- contract (iterating `Element._behaviorRegistry` calls `behavior.shouldAttach`
--- and `behavior.onAttach` / `behavior.onUpdate` directly). Falls through to the
--- frozen behavior instance for every hook.
-setmetatable(Animated, {
- __index = behavior,
- __tostring = function()
- return "Animated"
- end,
-})
-
-return Animated
diff --git a/libs/flexlove/modules/behaviors/Clickable.lua b/libs/flexlove/modules/behaviors/Clickable.lua
deleted file mode 100644
index 811ae11e..00000000
--- a/libs/flexlove/modules/behaviors/Clickable.lua
+++ /dev/null
@@ -1,344 +0,0 @@
--- modules/behaviors/Clickable.lua
---
--- Concrete behavior: mouse/touch event handling, pressed-state tracking,
--- hit-testing, and theme-state sync.
---
--- This is the largest behavior in the behavior-mode-unification refactor
--- (~200 LOC moved out of Element:update / _initSubSystems / saveState).
--- Task 02 extracts the entire `if self.onEvent or self.themeComponent or
--- self.editable or self._selectState or self.selectOption then ... end` block
--- from Element:update (hit-testing, mouse/touch event processing, immediate-
--- mode state save, theme-state update) plus EventHandler creation (formerly the
--- first half of Element:_initSubSystems) plus pressed-state drawing (formerly a
--- render layer in Renderer) plus EventHandler save/restore.
---
--- Attachment rule (shouldAttach): the same predicate that previously guarded
--- mouse-event processing in Element:update. An element owns the EventHandler /
--- gets press feedback exactly when it is interactive: when it declares an
--- `onEvent` callback, a `themeComponent`, is `editable`, or participates in a
--- Select group (selectParent / selectOption). A plain passive element never
--- attaches Clickable and therefore never allocates an EventHandler.
---
--- Element retains only the `self._eventHandler` field; Clickable owns it on
--- attach. All other Element paths that touched the EventHandler (handleTouchEvent,
--- handleGesture, getTouches) already nil-guard `self._eventHandler`, so they keep
--- working unchanged for non-clickable elements.
---
--- State ownership (per the locked Behavior contract):
--- * Per-element runtime state lives ON THE ELEMENT (self._eventHandler etc.).
--- * The behavior instance itself is stateless and shared across elements.
--- * Element-class-level dependencies (EventHandler factory, StateManager,
--- Context) are resolved from the owning element's metatable (the Element
--- class set by Element:_construct). This keeps the behavior stateless while
--- avoiding a dependency-injection parameter that would violate the locked
--- 6-hook signature `(element, ...)`.
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Resolve the Element class from an element instance.
--- Element instances are created via `setmetatable({}, Element)` in _construct,
--- so their metatable IS the Element class — giving us Element._EventHandler,
--- Element._eventHandlerDeps, Element._StateManager, Element._Context, etc.
--- without threading deps through the behavior hook signature.
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ----------------------------------------------------------------------------
--- shouldAttach (class-level predicate, no element required)
--- ----------------------------------------------------------------------------
-
--- Mirrors the cases that previously caused Element to allocate + use an
--- EventHandler. MUST cover every element that touches the EventHandler at
--- runtime: click (onEvent), theme press-feedback (themeComponent), text mouse
--- interaction (editable), Select groups (selectParent / selectOption), touch
--- callbacks (onTouchEvent), and gesture callbacks (onGesture). selectParent /
--- selectOption are the props that produce _selectState during _initSubSystems;
--- checking the props (rather than the runtime _selectState) lets shouldAttach
--- run before the Select subsystem is initialized.
-local function shouldAttach(props)
- props = props or {}
- return props.onEvent ~= nil
- or props.themeComponent ~= nil
- or props.editable == true
- or props.onTouchEvent ~= nil
- or props.onGesture ~= nil
- or props.selectOption ~= nil
- or props.selectParent ~= nil
-end
-
--- ----------------------------------------------------------------------------
--- onAttach — create the EventHandler (formerly Element:_initSubSystems
--- lines ~640-690) and restore immediate-mode EventHandler state.
--- ----------------------------------------------------------------------------
-
-local function onAttach(element)
- local Element = ElementClass(element)
-
- local eventHandlerConfig = {
- -- element.onEvent is source of truth; not cached on handler
- onEventDeferred = element.onEventDeferred,
- -- element.onTouchEvent is source of truth; not cached on handler
- onTouchEventDeferred = element.onTouchEventDeferred,
- -- element.onGesture is source of truth; not cached on handler
- onGestureDeferred = element.onGestureDeferred,
- touchEnabled = element.touchEnabled,
- multiTouchEnabled = element.multiTouchEnabled,
- }
-
- -- In immediate mode, restore EventHandler state from StateManager so pressed
- -- / hovered / click-count survive the per-frame element recreation cycle.
- -- Mode-aware via Context.isImmediateMode (behavior-mode-unification task 11):
- -- in retained mode the eventHandler persists, so nothing to restore.
- if Element._Context.isImmediateMode() and element._stateId and element._stateId ~= "" then
- local state = Element._StateManager.getState(element._stateId)
- if state then
- -- Restore EventHandler state from StateManager (sparse storage — provide defaults)
- eventHandlerConfig._pressed = state._pressed or {}
- eventHandlerConfig._lastClickTime = state._lastClickTime
- eventHandlerConfig._lastClickButton = state._lastClickButton
- eventHandlerConfig._clickCount = state._clickCount or 0
- eventHandlerConfig._dragStartX = state._dragStartX or {}
- eventHandlerConfig._dragStartY = state._dragStartY or {}
- eventHandlerConfig._lastMouseX = state._lastMouseX or {}
- eventHandlerConfig._lastMouseY = state._lastMouseY or {}
- eventHandlerConfig._hovered = state._hovered
- end
- end
-
- element._eventHandler = Element._EventHandler.new(eventHandlerConfig, Element._eventHandlerDeps)
-end
-
-local function onDetach(element)
- -- Clear focus callbacks read by KeyboardNavigation / TextEditor:focus so the
- -- element's closure references can be collected in immediate mode (formerly
- -- part of Element:_cleanup). The EventHandler instance itself is INTENTIONALLY
- -- kept: Element:_cleanup preserves element structure for inspection (the
- -- stale-element refs are released when the element is GC'd). onEvent,
- -- onTouchEvent, onGesture are also left intact — the Renderer/EventHandler
- -- read those directly from the element (not the cache), so clearing them
- -- would break retained mode.
- element.onFocus = nil
- element.onBlur = nil
-end
-
--- ----------------------------------------------------------------------------
--- onUpdate — the mouse hit-testing + event-processing + theme-state +
--- immediate-mode save block (formerly Element:update lines ~2813-2960).
--- ----------------------------------------------------------------------------
-
-local function onUpdate(element, dt)
- local Element = ElementClass(element)
- local eventHandler = element._eventHandler
- if not eventHandler then
- return
- end
-
- local mx, my = love.mouse.getPosition()
-
- -- Clickable area is the border box (x, y already includes padding)
- -- BORDER-BOX MODEL: Use stored border-box dimensions for hit detection
- local bx = element.x
- local by = element.y
- local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
-
- -- Account for scroll offsets from parent containers
- -- Walk up the parent chain and accumulate scroll offsets. This stays in
- -- Clickable because it's an interaction concern (hit-testing), not layout.
- local scrollOffsetX = 0
- local scrollOffsetY = 0
- local current = element.parent
- while current do
- local overflowX = current.overflowX or current.overflow
- local overflowY = current.overflowY or current.overflow
- local hasScrollableOverflow = (
- overflowX == "scroll"
- or overflowX == "auto"
- or overflowY == "scroll"
- or overflowY == "auto"
- or overflowX == "hidden"
- or overflowY == "hidden"
- )
- if hasScrollableOverflow then
- scrollOffsetX = scrollOffsetX + (current._scrollX or 0)
- scrollOffsetY = scrollOffsetY + (current._scrollY or 0)
- end
- current = current.parent
- end
-
- -- Adjust mouse position by accumulated scroll offset for hit testing
- local adjustedMx = mx + scrollOffsetX
- local adjustedMy = my + scrollOffsetY
- local isHovering = adjustedMx >= bx and adjustedMx <= bx + bw and adjustedMy >= by and adjustedMy <= by + bh
-
- -- Check if this is the topmost interactive element at the mouse position
- -- (z-index ordering). This prevents blocked/occluded elements from
- -- receiving interactions or visual feedback. A single mode-agnostic lookup
- -- via `Context.findInteractiveAtPosition` (unified-event-routing task 05)
- -- replaces the previous immediate/retained-mode split that used
- -- `getTopElementAt` in immediate mode and `_activeEventElement` in retained
- -- mode. `findInteractiveAtPosition` routes every hit test through
- -- `pointHitsElement` (the single canonical `display == false` guard) and
- -- resolves occlusion by z-index in both modes, so the active element is the
- -- same one that would receive a hit under the cursor.
- local topElement = Element._Context.findInteractiveAtPosition(mx, my)
- local isActiveElement = (topElement == element or topElement == nil)
-
- -- Reset scrollbar press flag at start of each frame
- eventHandler:resetScrollbarPressFlag()
-
- -- Process mouse events through EventHandler FIRST
- -- This ensures pressed states are updated before theme state is calculated
- eventHandler:processMouseEvents(element, mx, my, isHovering, isActiveElement)
-
- -- In immediate mode, save EventHandler state to StateManager after
- -- processing events so it survives the per-frame recreation.
- if element._stateId and Element._Context.isImmediateMode() and element._stateId ~= "" then
- local eventHandlerState = eventHandler:getState()
- Element._StateManager.updateState(element._stateId, {
- _pressed = eventHandlerState._pressed,
- _lastClickTime = eventHandlerState._lastClickTime,
- _lastClickButton = eventHandlerState._lastClickButton,
- _clickCount = eventHandlerState._clickCount,
- _dragStartX = eventHandlerState._dragStartX,
- _dragStartY = eventHandlerState._dragStartY,
- _lastMouseX = eventHandlerState._lastMouseX,
- _lastMouseY = eventHandlerState._lastMouseY,
- _hovered = eventHandlerState._hovered,
- })
- end
-
- -- Update theme state based on interaction. themeComponent state update
- -- lives in Clickable because it is driven by hover/press state; the actual
- -- theme RENDERING is the Themed behavior (task 07).
- if element.themeComponent then
- -- Check if any button is pressed via EventHandler
- local anyPressed = eventHandler:isAnyButtonPressed()
-
- -- Update theme state via ThemeManager
- local isFocused = Element._Context.getFocused() == element
- local newThemeState =
- element._themeManager:updateState(isHovering and isActiveElement, anyPressed, isFocused, element.disabled)
-
- if element._stateId and Element._Context.isImmediateMode() then
- local hover = newThemeState == "hover"
- local pressed = newThemeState == "pressed"
- local focused = isFocused
-
- Element._StateManager.updateState(element._stateId, {
- hover = hover,
- pressed = pressed,
- focused = focused,
- disabled = element.disabled,
- active = element.active,
- })
- end
-
- if element._renderer then
- element._renderer:setThemeState(newThemeState)
- end
- end
-
- -- Process touch events through EventHandler
- eventHandler:processTouchEvents(element)
-end
-
--- ----------------------------------------------------------------------------
--- onDraw — pressed-state visual feedback (formerly Renderer Layer 5).
--- ----------------------------------------------------------------------------
-
--- Draws the grey pressed overlay when any mouse button is currently pressed on
--- the element. Delegates the actual pixels to Renderer:drawPressedState (which
--- owns the RoundedRect + opacity math) but drives the DECISION + transform
--- context here, so the renderer no longer needs the `if element.onEvent ...`
--- behavioral branch. Honors disableHighlight (themes handle their own visual
--- feedback) exactly as the old render layer did.
-local function onDraw(element)
- if element.disableHighlight then
- return
- end
- local eventHandler = element._eventHandler
- if not eventHandler then
- return
- end
-
- local anyPressed = false
- local pressedState = eventHandler:getState()._pressed or {}
- for _, pressed in pairs(pressedState) do
- if pressed then
- anyPressed = true
- break
- end
- end
- if not anyPressed then
- return
- end
-
- local renderer = element._renderer
- if not renderer then
- return
- end
-
- local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
- local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
-
- -- Apply the element transform around the overlay, mirroring how the
- -- Renderer wrapped its whole command buffer (pressed state was a render
- -- layer subject to the same transform).
- local Element = ElementClass(element)
- local Transform = Element._Transform
- local hasTransform = element.transform ~= nil and Transform ~= nil and not Transform.isIdentity(element.transform)
- if hasTransform then
- Transform.apply(element.transform, element.x, element.y, element.width, element.height)
- end
-
- renderer:drawPressedState(element.x, element.y, bw, bh, element.opacity, element.cornerRadius)
-
- if hasTransform then
- Transform.unapply()
- end
-end
-
--- ----------------------------------------------------------------------------
--- saveState / restoreState — EventHandler state (formerly the eventHandler
--- branches of Element:saveState / Element:restoreState).
--- ----------------------------------------------------------------------------
-
-local function saveState(element)
- if element._eventHandler then
- return { eventHandler = element._eventHandler:getState() }
- end
- return nil
-end
-
-local function restoreState(element, state)
- if not state then
- return nil
- end
- if element._eventHandler and state.eventHandler then
- element._eventHandler:setState(state.eventHandler)
- end
- return nil
-end
-
--- ----------------------------------------------------------------------------
--- Build the (stateless, shared, immutable) behavior instance.
--- ----------------------------------------------------------------------------
-
-local Clickable = Behavior.new({
- onAttach = onAttach,
- onDetach = onDetach,
- onUpdate = onUpdate,
- onDraw = onDraw,
- saveState = saveState,
- restoreState = restoreState,
- shouldAttach = shouldAttach,
-})
-
--- Expose the predicate at module level so callers/tests can reference it
--- directly without an element instance (mirrors Behavior.shouldAttach).
-Clickable.shouldAttach = shouldAttach
-
-return Clickable
diff --git a/libs/flexlove/modules/behaviors/Imageable.lua b/libs/flexlove/modules/behaviors/Imageable.lua
deleted file mode 100644
index b448a47b..00000000
--- a/libs/flexlove/modules/behaviors/Imageable.lua
+++ /dev/null
@@ -1,282 +0,0 @@
--- modules/behaviors/Imageable.lua
---
--- Concrete behavior: image loading + image rendering config.
---
--- Imageable owns the image side of the Renderer: it runs the deferred image-
--- load pipeline (cache check → defer → load → fire onImageLoad/onImageError
--- callbacks), populates the resolved `_loadedImage` cache on both the element
--- and the shared renderer, and persists that cache across immediate-mode
--- recreation. It is the behavior-mode-unification replacement for the image-
--- loading half of Element:_initImageAndRenderer and the deferred
--- Element:_loadImage method (behavior-mode-unification task 07).
---
--- Image value props (imagePath/image/objectFit/objectPosition/imageOpacity/
--- imageRepeat/imageTint) are bound on the ELEMENT by Element:_applyProps and read
--- from the element at draw time (Renderer._executeDrawCommand image branch) —
--- Imageable does NOT mirror them onto the renderer, so bare writes and
--- setProperty(...) are immediately consistent. Only the resolved _loadedImage
--- cache (the love.Image produced by the load pipeline) is renderer-mirrored,
--- because Renderer:draw reads `self._loadedImage`.
---
--- Runtime reload: setProperty("imagePath", ...) / setProperty("image", ...) and
--- the bare-write-equivalent setImage* flows route through element._reloadImage
--- (installed below) which re-runs the load pipeline. See
--- TestRetainedPropertyConsistency (image props) and TestImageableIntegration.
---
--- Attachment rule (shouldAttach): an element owns image concern exactly when it
--- declares an `imagePath` (load-from-path) or a direct `image` (already-loaded
--- love.Image). Mirrors the old `if self.imagePath / if self.image` init branches.
---
--- Pairing with Themed: Themed.onAttach creates the Renderer with theme/blur
--- config; Imageable.onAttach enriches the SAME renderer instance with image
--- config + kicks off loading. They share `element._renderer`. In the registry
--- Imageable runs after Themed, so the renderer already exists; the create-or-
--- reuse guard below covers the defensive case where Imageable attaches first.
---
--- onDraw: the image LAYER is rendered by the integrated `Renderer:draw` call
--- (owned by the Themed behavior) which executes the renderer's `image` draw
--- command using the config Imageable.onAttach wired. Imageable.onDraw is
--- therefore a no-op for the draw call itself — there is no separate
--- `_renderer:_drawImage` entry point; pixel emission lives in the integrated
--- Renderer:draw command buffer. Splitting it out would require Renderer surgery
--- with no behavioral gain (Renderer:draw already conditionally skips the image
--- layer when no image is loaded).
---
--- State ownership (per the locked Behavior contract):
--- * Per-element runtime state lives ON THE ELEMENT (`element._loadedImage`,
--- `element._renderer._loadedImage`). The behavior instance is stateless.
--- * saveState/restoreState persist `_loadedImage` across immediate-mode frames
--- so the image renders even if the ImageCache is cleared between frames and
--- so the renderer's loaded-image cache survives element recreation.
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Lua 5.4 removed the global `unpack`; mirror Element's alias.
-local unpack = table.unpack or unpack
-
--- Resolve the Element class from an element instance (mirrors Clickable/Themed).
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ----------------------------------------------------------------------------
--- shouldAttach (class-level predicate, no element required)
--- ----------------------------------------------------------------------------
-
-local function shouldAttach(props)
- props = props or {}
- return props.imagePath ~= nil or props.image ~= nil
-end
-
--- ----------------------------------------------------------------------------
--- Image callback helper (moved from Element._fireImageCallback).
--- Fires a user-supplied image callback (onImageLoad/onImageError) under pcall,
--- honoring the onXDeferred flag when `honorDeferred` is true, and emits a single
--- EVT_002 warn on failure. The direct-`image` sync init path passes
--- honorDeferred=false to preserve immediate firing (image is already loaded).
--- ----------------------------------------------------------------------------
-
-local function fireImageCallback(element, callbackField, honorDeferred, ...)
- local cb = element[callbackField]
- if type(cb) ~= "function" then
- return
- end
- local Element = ElementClass(element)
- local argc = select("#", ...)
- local args = { ... }
- local function invoke()
- local ok, err = pcall(cb, element, unpack(args, 1, argc))
- if not ok then
- Element._ErrorHandler:warn("Element", "EVT_002", {
- callback = callbackField,
- error = tostring(err),
- })
- end
- end
- if honorDeferred and element[callbackField .. "Deferred"] then
- Element._Context.deferCallback(invoke)
- else
- invoke()
- end
-end
-
--- ----------------------------------------------------------------------------
--- Deferred image loader (replaces Element:_loadImage).
---
--- Invoked by Element's deferred-method dispatcher via the instance closure that
--- onAttach installs on `element._loadImage`. Loads the image from cache or disk
--- (I/O), updates BOTH the element and renderer `_loadedImage` caches so the
--- image draws after an async load, and fires the load/error callback (deferred,
--- honoring onImageLoadDeferred / onImageErrorDeferred).
--- ----------------------------------------------------------------------------
-
-local function loadImage(element)
- if not element.imagePath or element.image then
- return
- end
- local Element = ElementClass(element)
- local loadedImage, err = Element._ImageCache.load(element.imagePath)
- if loadedImage then
- element._loadedImage = loadedImage
- if element._renderer then
- element._renderer._loadedImage = loadedImage
- end
- fireImageCallback(element, "onImageLoad", true, loadedImage)
- else
- fireImageCallback(element, "onImageError", true, err or "Unknown error")
- end
-end
-
--- ----------------------------------------------------------------------------
--- reloadImage — recompute the loaded-image cache from the current image/imagePath.
---
--- This is the single entry point for (re)loading after either initial attach or
--- a runtime property change (see Element._specialSetHandlers.imagePath/image,
--- which call element:_reloadImage()). Precedence matches onAttach: a direct
--- `image` wins over `imagePath`; `nil` for both clears the cache.
---
--- * direct image → set _loadedImage immediately, fire onImageLoad SYNC (the
--- image is already loaded; honorDeferred=false preserves the
--- original synchronous init contract).
--- * imagePath → cache CHECK only (no I/O) so a cached image can draw this
--- frame, then defer the loader (_loadImage) for the actual
--- I/O + deferred callbacks. load bails if `image` is later set.
--- * neither → clear _loadedImage on both element + renderer.
---
--- Image value props (objectFit/imageOpacity/imageRepeat/imageTint/objectPosition)
--- and imagePath/image themselves live on the ELEMENT as source of truth; the
--- renderer reads them at draw time, so reloadImage does NOT mirror them onto the
--- renderer — only the resolved _loadedImage cache is pushed.
--- ----------------------------------------------------------------------------
-
-local function reloadImage(element)
- local Element = ElementClass(element)
- local renderer = element._renderer
- if element.image then
- element._loadedImage = element.image
- if renderer then
- renderer._loadedImage = element.image
- end
- fireImageCallback(element, "onImageLoad", false, element.image)
- elseif element.imagePath then
- -- Cache check (no I/O). Populate both caches immediately if cached so the
- -- image can draw this frame without waiting for the deferred load.
- local cached = Element._ImageCache.get(element.imagePath)
- element._loadedImage = cached
- if renderer then
- renderer._loadedImage = cached
- end
- -- Kick off the deferred I/O load + callbacks (idempotent: loadImage bails
- -- if image is set or imagePath is nil by the time it runs).
- if element._loadImage then
- element:_deferMethod("_loadImage")
- end
- else
- element._loadedImage = nil
- if renderer then
- renderer._loadedImage = nil
- end
- end
-end
-
--- ----------------------------------------------------------------------------
--- onAttach — enrich the shared renderer with image config + kick off loading
--- (formerly the image block of Element:_initImageAndRenderer).
--- ----------------------------------------------------------------------------
-
-local function onAttach(element)
- local Element = ElementClass(element)
-
- -- Ensure the renderer exists (Thamed normally creates it; this create-or-reuse
- -- guard is defensive for the Imageable-attaches-first ordering).
- if not element._renderer then
- element._renderer = Element._Renderer.new({
- theme = element.theme,
- scaleCorners = element.scaleCorners,
- scalingAlgorithm = element.scalingAlgorithm,
- contentBlur = element.contentBlur,
- backdropBlur = element.backdropBlur,
- }, Element._rendererDeps)
- end
-
- -- Install the (re)load hooks as instance methods so Element's
- -- deferred-method dispatcher / setProperty special handlers can trigger a
- -- reload without Element needing a behavior reference. This keeps Element
- -- decoupled from the Imageable behavior (mirrors the stateless-behavior +
- -- element-owned-state contract). Image value props and imagePath/image live
- -- on the element as source of truth (read at draw time); only the resolved
- -- _loadedImage cache is mirrored onto the renderer by reloadImage.
- element._loadImage = function(el)
- loadImage(el)
- end
- element._reloadImage = function(el)
- reloadImage(el)
- end
-
- -- Initial load: compute _loadedImage + defer the I/O load.
- reloadImage(element)
-end
-
--- ----------------------------------------------------------------------------
--- onDraw — no-op (see file header: the image layer is rendered by the integrated
--- Renderer:draw call owned by the Themed behavior, using the config wired here).
--- ----------------------------------------------------------------------------
-
--- ----------------------------------------------------------------------------
--- saveState / restoreState — `_loadedImage` cache (for immediate-mode).
--- ----------------------------------------------------------------------------
-
-local function saveState(element)
- if element._loadedImage ~= nil then
- return { _loadedImage = element._loadedImage }
- end
- return nil
-end
-
-local function restoreState(element, state)
- if not state or state._loadedImage == nil then
- return nil
- end
- local loadedImage = state._loadedImage
- element._loadedImage = loadedImage
- if element._renderer then
- element._renderer._loadedImage = loadedImage
- end
- return nil
-end
-
--- ----------------------------------------------------------------------------
--- onDetach — release image-load callback closures so the element can be GC'd
--- cleanly in immediate mode (formerly part of Element:_cleanup). The cached
--- `_loadedImage` is reproduced on the next attach via the Imageable saveState
--- -> restoreState cycle, so dropping the live references is always safe.
--- ----------------------------------------------------------------------------
-
-local function onDetach(element)
- element.onImageLoad = nil
- element.onImageError = nil
-end
-
--- ----------------------------------------------------------------------------
--- Build the (stateless, shared, immutable) behavior instance.
--- ----------------------------------------------------------------------------
-
-local Imageable = Behavior.new({
- onAttach = onAttach,
- onDetach = onDetach,
- onUpdate = function() end,
- onDraw = function() end,
- saveState = saveState,
- restoreState = restoreState,
- shouldAttach = shouldAttach,
-})
-
--- Expose the predicate at module level so callers/tests can reference it
--- directly without an element instance (mirrors Behavior.shouldAttach /
--- Clickable.shouldAttach). `loadImage` is NOT exposed on the (frozen) behavior
--- instance; it is captured as a module-local upvalue by the onAttach closure that
--- installs `element._loadImage`.
-Imageable.shouldAttach = shouldAttach
-
-return Imageable
diff --git a/libs/flexlove/modules/behaviors/Persistable.lua b/libs/flexlove/modules/behaviors/Persistable.lua
deleted file mode 100644
index 6bcdd0de..00000000
--- a/libs/flexlove/modules/behaviors/Persistable.lua
+++ /dev/null
@@ -1,132 +0,0 @@
--- modules/behaviors/Persistable.lua
---
--- Concrete behavior: generic public-property persistence across the immediate-
--- mode recreation cycle (behavior-mode-unification task 12).
---
--- Owns the ONE piece of Element save/restore state that is NOT subsystem state:
--- the snapshot of an element's own public scalar fields (`text`, `display`,
--- `opacity`, `x`, `width`, ...). Event-driven mutations to these fields (a
--- release callback changing `text`, a toggle hiding a panel via `display =
--- false`) must survive the per-frame Element recreation that defines immediate
--- mode. Persistable captures them in `saveState` and reapplies them in
--- `restoreState`, so the caller never branches on mode.
---
--- This behavior is the final home for the former `Element:saveState` `_props`
--- block and the former `Element:restoreState` `_props` block (~20 LOC moved out
--- of Element.lua). With it in place, `Element:saveState` / `Element:restoreState`
--- collapse to a pure behavior-dispatch loop and Element owns zero property-
--- extraction logic — every persisted slice is owned by exactly one behavior.
---
--- Attachment rule (shouldAttach): every element. Persistable attaches
--- unconditionally (mirrors the pre-refactor invariant that every element's
--- public scalar props were scanned). The actual snapshot is mode-gated inside
--- `saveState` (immediate-mode-only, matching the legacy contract); in retained
--- mode `saveState` returns nil and `restoreState` is a no-op unless a snapshot
--- is explicitly passed.
---
--- Registry ordering: Persistable is intentionally placed LAST in the behavior
--- registry. `restoreState` applies `_props` AFTER every other behavior has
--- hydrated its subsystem state, so a persisted public-prop mutation (e.g.
--- `text = "mutated"`) overrides the freshly-restored TextEditor/Select state —
--- preserving the legacy restore ordering (behaviors first, `_props` tail).
---
--- State ownership (per the locked Behavior contract):
--- * The persisted props live ON the element (they ARE the element's public
--- fields). The behavior instance is stateless + immutable and shared.
--- * The snapshot is returned under the `_props` key (prefixed with `_` so
--- the public-prop scan itself skips it — avoiding self-recursion).
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Resolve the Element class from an element instance (mirrors Clickable /
--- Themed). Element instances are created via `setmetatable({}, Element)`, so
--- their metatable IS the Element class — giving access to Element._StateManager
--- without threading deps through the hook signature.
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ============================================================================
--- shouldAttach (class-level predicate, no element required)
--- ============================================================================
-
--- Every element's public scalar props are persistable, so this behavior
--- attaches unconditionally. The mode gate lives inside saveState (it needs the
--- runtime mode, which is only available with an element via StateManager).
-local function shouldAttach()
- return true
-end
-
--- ============================================================================
--- saveState — snapshot public scalar fields (immediate-mode-only).
--- ============================================================================
-
--- Mirrors the former `Element:saveState` `_props` block exactly:
--- * Only string keys NOT prefixed with `_` (so internal fields like
--- `_renderer`, `_themeState`, `_initProps` are excluded).
--- * Only scalar values (numbers, strings, booleans); tables and functions
--- are excluded (children, padding, onEvent, ...).
--- Returns `{ _props = {...} }` when there is at least one persistable prop and
--- the element is in immediate mode; nil otherwise (retained mode no-op —
--- state lives on the element directly there, so nothing to snapshot).
-local function saveState(element)
- local Element = ElementClass(element)
- if not Element._StateManager.isImmediateMode() then
- return nil
- end
- local props = {}
- for k, v in pairs(element) do
- if type(k) == "string" and k:sub(1, 1) ~= "_" and type(v) ~= "table" and type(v) ~= "function" then
- props[k] = v
- end
- end
- if next(props) then
- return { _props = props }
- end
- return nil
-end
-
--- ============================================================================
--- restoreState — reapply the persisted public-prop snapshot onto a fresh
--- element (mode-agnostic; only fires when a `_props` slice is present).
--- ============================================================================
-
--- Applies persisted mutations on top of whatever the constructor + other
--- behaviors already set, so event-driven changes from the previous frame
--- override the declarative props of the recreated element. Runs last in the
--- behavior dispatch (Persistable is the registry tail) to preserve the legacy
--- restore ordering (subsystem restore first, `_props` override last).
-local function restoreState(element, state)
- if not state or not state._props then
- return
- end
- for k, v in pairs(state._props) do
- element[k] = v
- end
-end
-
--- ============================================================================
--- onAttach / onUpdate / onDraw / onDetach — no-ops.
--- ============================================================================
-
--- Persistable owns no subsystem and allocates no per-element state (the
--- "state" it persists IS the element's own fields). The lifecycle is purely
--- save/restore.
-
--- ============================================================================
--- Build the (stateless, shared, immutable) behavior instance.
--- ============================================================================
-
-local Persistable = Behavior.new({
- saveState = saveState,
- restoreState = restoreState,
- shouldAttach = shouldAttach,
-})
-
--- Expose the predicate at module level so callers/tests can reference it
--- directly without an element instance (mirrors Behavior.shouldAttach /
--- Clickable.shouldAttach).
-Persistable.shouldAttach = shouldAttach
-
-return Persistable
diff --git a/libs/flexlove/modules/behaviors/Scrollable.lua b/libs/flexlove/modules/behaviors/Scrollable.lua
deleted file mode 100644
index 7c909e72..00000000
--- a/libs/flexlove/modules/behaviors/Scrollable.lua
+++ /dev/null
@@ -1,264 +0,0 @@
--- modules/behaviors/Scrollable.lua
---
--- Concrete behavior: ScrollManager lifecycle (creation + immediate-mode
--- scrollbar interaction-state restore).
---
--- Scrollable owns the per-element ScrollManager instance — the subsystem that
--- manages overflow detection, scrollbar geometry, scroll position, and scrollbar
--- drag/hover interaction. It is the behavior-mode-unification replacement for
--- the former `Element:_initScrollManager` phase (~84 LOC) of Element.new
--- (behavior-mode-unification task 03 / landed as part of the task 08 capstone).
---
--- Attachment rule (shouldAttach): an element owns a ScrollManager exactly when
--- it declares an `overflow`, `overflowX`, or `overflowY` prop — mirroring the
--- legacy `if props.overflow or props.overflowX or props.overflowY then` guard
--- in `Element:_initScrollManager`. The ScrollManager is created and its
--- normalized fields are exposed back onto the element (so the Renderer /
--- ScrollManager delegates read `element.overflow` / `element.scrollbarWidth`
--- etc.) exactly as the legacy inline phase did.
---
--- Why onAttach reads `element._initProps` (not element fields): the scrollbar
--- configuration props (scrollbarWidth / scrollbarColor / scrollSpeed /
--- scrollbarPlacement / scrollbarBalance / invertScroll / smoothScrollEnabled /
--- scrollBarStyle / scrollbarKnobOffset / hideScrollbars / scrollbarRadius /
--- scrollbarPadding / scrollbarTrackColor / _scrollX / _scrollY) are listed in
--- SPECIAL_PROPS and therefore NOT bound onto the element by the schema-driven
--- `_applyProps` loop — they are consumed only by the ScrollManager constructor.
--- The locked behavior hook signature is `(element, ...)` with no props arg, so
--- the original construction props are stashed on the element as `_initProps` by
--- `Element:_construct` and read back here. (`overflow` / `overflowX` /
--- `overflowY` ARE bound onto the element by `_applyProps` so that
--- `Element:addChild`'s scroll-container auto-size guard sees them during
--- declarative-children processing in `_finalizeConstruction`, which runs BEFORE
--- this onAttach; onAttach then overwrites them with the ScrollManager's
--- normalized values, matching the legacy field-exposure order.)
---
--- onUpdate / onDraw / saveState / restoreState are deferred to the
--- behavior-driven update/draw tasks (09 / 12): the ScrollManager update,
--- interaction, scrollbar drawing, and state save/restore currently stay inline
--- in `Element:update` / `Element:draw` / `Element:saveState` /
--- `Element:restoreState` (delegated through the ScrollManager API bound in
--- `Element.init`). Those inline call sites are NOT behavioral `if` branches —
--- they are unconditional 1-line delegates — so leaving them in Element does not
--- regress the behavior-dispatch goals of tasks 09/12; task 09 will fold them
--- into Scrollable hooks.
---
--- State ownership (per the locked Behavior contract):
--- * Per-element runtime state lives ON THE ELEMENT (`element._scrollManager`,
--- `element.overflow`, `element._scrollX`, `element._scrollbarDragging`, ...).
--- * The behavior instance is stateless + immutable and shared across elements.
--- * Element-class-level dependencies (`Element._ScrollManager`,
--- `Element._scrollManagerDeps`, `Element._Context`, `Element._StateManager`)
--- are resolved from the owning element's metatable (the Element class set by
--- `Element:_construct`).
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Resolve the Element class from an element instance.
--- `setmetatable({}, Element)` in `_construct` makes the instance metatable BE
--- the Element class, so this yields Element._ScrollManager,
--- Element._scrollManagerDeps, Element._Context, Element._StateManager without
--- threading deps through the hook signature.
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ----------------------------------------------------------------------------
--- shouldAttach (class-level predicate, no element required)
--- ----------------------------------------------------------------------------
-
--- Mirrors the legacy `if props.overflow or props.overflowX or props.overflowY`
--- guard. Uses `~= nil` (rather than truthiness) so that an explicit
--- `overflow = false` / `overflow = ""` does not spuriously attach — though in
--- practice overflow values are always strings or unset, matching the predicate
--- semantics of the other behaviors (Clickable / TextEditable / Selectable).
-local function shouldAttach(props)
- props = props or {}
- return props.overflow ~= nil or props.overflowX ~= nil or props.overflowY ~= nil
-end
-
--- ----------------------------------------------------------------------------
--- onAttach — create the ScrollManager + expose its fields + restore immediate-
--- mode scrollbar interaction state (formerly Element:_initScrollManager).
--- ----------------------------------------------------------------------------
-
-local function onAttach(element)
- local Element = ElementClass(element)
- -- Construction props are stashed on the element by _construct (the scrollbar
- -- config props are SPECIAL_PROPS and not bound as element fields).
- local props = element._initProps or {}
-
- element._scrollManager = Element._ScrollManager.new({
- overflow = props.overflow,
- overflowX = props.overflowX,
- overflowY = props.overflowY,
- scrollbarWidth = props.scrollbarWidth,
- scrollbarColor = props.scrollbarColor,
- scrollbarTrackColor = props.scrollbarTrackColor,
- scrollbarRadius = props.scrollbarRadius,
- scrollbarPadding = props.scrollbarPadding,
- scrollSpeed = props.scrollSpeed,
- invertScroll = props.invertScroll,
- smoothScrollEnabled = props.smoothScrollEnabled,
- scrollBarStyle = props.scrollBarStyle,
- scrollbarKnobOffset = props.scrollbarKnobOffset,
- hideScrollbars = props.hideScrollbars,
- scrollbarPlacement = props.scrollbarPlacement,
- scrollbarBalance = props.scrollbarBalance,
- _scrollX = props._scrollX,
- _scrollY = props._scrollY,
- }, Element._scrollManagerDeps)
-
- -- Expose ScrollManager properties for backward compatibility (Renderer access).
- local sm = element._scrollManager
- element.overflow = sm.overflow
- element.overflowX = sm.overflowX
- element.overflowY = sm.overflowY
- element.scrollbarWidth = sm.scrollbarWidth
- element.scrollbarColor = sm.scrollbarColor
- element.scrollbarTrackColor = sm.scrollbarTrackColor
- element.scrollbarRadius = sm.scrollbarRadius
- element.scrollbarPadding = sm.scrollbarPadding
- element.scrollSpeed = sm.scrollSpeed
- element.invertScroll = sm.invertScroll
- element.scrollBarStyle = sm.scrollBarStyle
- element.scrollbarKnobOffset = sm.scrollbarKnobOffset
- element.hideScrollbars = sm.hideScrollbars
- element.scrollbarPlacement = sm.scrollbarPlacement
- element.scrollbarBalance = sm.scrollbarBalance
-
- -- Initialize state properties (will be synced from ScrollManager).
- element._overflowX = false
- element._overflowY = false
- element._contentWidth = 0
- element._contentHeight = 0
- element._scrollX = 0
- element._scrollY = 0
- element._maxScrollX = 0
- element._maxScrollY = 0
- element._scrollbarHoveredVertical = false
- element._scrollbarHoveredHorizontal = false
- element._scrollbarDragging = false
- element._hoveredScrollbar = nil
- element._scrollbarDragOffset = 0
-
- -- Restore scrollbar state from StateManager in immediate mode (must happen
- -- before layout). Mirrors the legacy _initScrollManager restore block.
- -- Mode-aware via Context.isImmediateMode (behavior-mode-unification task 11).
- if Element._Context.isImmediateMode() and element._stateId and element._stateId ~= "" then
- local state = Element._StateManager.getState(element._stateId)
- if state and state.scrollManager then
- element._scrollbarHoveredVertical = state.scrollManager._scrollbarHoveredVertical or false
- element._scrollbarHoveredHorizontal = state.scrollManager._scrollbarHoveredHorizontal or false
- element._scrollbarDragging = state.scrollManager._scrollbarDragging or false
- element._hoveredScrollbar = state.scrollManager._hoveredScrollbar
- element._scrollbarDragOffset = state.scrollManager._scrollbarDragOffset or 0
-
- -- Apply to ScrollManager immediately.
- sm._scrollbarHoveredVertical = element._scrollbarHoveredVertical
- sm._scrollbarHoveredHorizontal = element._scrollbarHoveredHorizontal
- sm._scrollbarDragging = element._scrollbarDragging
- sm._hoveredScrollbar = element._hoveredScrollbar
- sm._scrollbarDragOffset = element._scrollbarDragOffset
-
- -- Restore drag start positions for relative movement tracking.
- sm._dragStartMouseX = state.scrollManager._dragStartMouseX or 0
- sm._dragStartMouseY = state.scrollManager._dragStartMouseY or 0
- sm._dragStartScrollX = state.scrollManager._dragStartScrollX or 0
- sm._dragStartScrollY = state.scrollManager._dragStartScrollY or 0
- end
- end
-end
-
--- --------------------------------------------------------------------------
--- onUpdate — scroll-position momentum + scrollbar hover/drag/press interaction
--- (formerly the inline ScrollManager blocks in Element:update).
--- Runs BEFORE Clickable.onUpdate in the registry so the scrollbar press flag
--- is set before Clickable's EventHandler processes mouse events.
--- --------------------------------------------------------------------------
-
-local function onUpdate(element, dt)
- local Element = ElementClass(element)
- local sm = element._scrollManager
- if not sm then
- return
- end
- -- Restore scrollbar interaction state from StateManager in immediate mode
- -- (no-op outside immediate mode / when no state is stored).
- Element._ScrollManager.restoreImmediateState(element)
-
- -- Smooth-scroll / momentum interpolation.
- sm:update(dt)
- element:_syncScrollManagerState()
-
- -- Scrollbar hover / drag / press interaction. Captures the mouse here so the
- -- interaction state is consistent across the rest of the frame's behaviors.
- local mx, my = love.mouse.getPosition()
- Element._ScrollManager.updateInteraction(element, mx, my)
-end
-
--- --------------------------------------------------------------------------
--- onDraw — scrollbar rendering (post-children overlay). Marked
--- `drawLayer = "overlay"` so Element:draw dispatches it AFTER children, so
--- scrollbars paint on top of clipped child content and without parent clipping.
--- --------------------------------------------------------------------------
-
-local function onDraw(element, _ctx)
- local overflowX = element.overflowX or element.overflow
- local overflowY = element.overflowY or element.overflow
- if overflowX ~= "scroll" and overflowX ~= "auto" and overflowY ~= "scroll" and overflowY ~= "auto" then
- return
- end
- local scrollbarDims = element:_calculateScrollbarDimensions()
- if not (scrollbarDims.vertical.visible or scrollbarDims.horizontal.visible) then
- return
- end
- -- Clear any parent scissor clipping before drawing scrollbars so they render
- -- fully visible (scrollbars must not be clipped by ancestor overflow).
- love.graphics.setScissor()
- element._renderer:drawScrollbars(element, element.x, element.y, element.width, element.height, scrollbarDims)
-end
-
--- --------------------------------------------------------------------------
--- saveState / restoreState — ScrollManager state snapshot for immediate-mode
--- recreation (formerly the inline blocks in Element:saveState/
--- Element:restoreState). Returns a table merged under the `scrollManager` key
--- by Element:saveState's behavior loop, mirroring the legacy contract.
--- --------------------------------------------------------------------------
-
-local function saveState(element)
- local sm = element._scrollManager
- if not sm then
- return nil
- end
- return { scrollManager = sm:getState() }
-end
-
-local function restoreState(element, state)
- if not state then
- return
- end
- local sm = element._scrollManager
- local smState = state.scrollManager
- if sm and smState then
- sm:setState(smState)
- end
-end
-
-local Scrollable = Behavior.new({
- onAttach = onAttach,
- onDetach = function() end,
- onUpdate = onUpdate,
- onDraw = onDraw,
- saveState = saveState,
- restoreState = restoreState,
- drawLayer = "overlay",
-})
-
--- Expose the predicate at module level so callers/tests can reference it
--- directly without an element instance (mirrors Clickable.shouldAttach /
--- Selectable.shouldAttach).
-Scrollable.shouldAttach = shouldAttach
-
-return Scrollable
diff --git a/libs/flexlove/modules/behaviors/Selectable.lua b/libs/flexlove/modules/behaviors/Selectable.lua
deleted file mode 100644
index e63bb4c6..00000000
--- a/libs/flexlove/modules/behaviors/Selectable.lua
+++ /dev/null
@@ -1,206 +0,0 @@
--- modules/behaviors/Selectable.lua
---
--- Concrete behavior: Select state-machine lifecycle for dropdown-style
--- select groups. Owns the per-element Select subsystem initialization, the
--- managed-frame layout sync each frame, and select save/restore across the
--- immediate-mode recreation cycle.
---
--- This behavior consolidates the legacy `if self._selectState` / `if
--- self.selectOption` branches that previously lived inside Element.lua:
---
--- * Select subsystem init (formerly Element:_initSubSystems lines ~810-825 —
--- `Select.initSelectParent` / `Select.initSelectOption`).
--- * Managed-frame adoption (formerly Element:_initPositioning lines ~1700-
--- 1702 — `Select.adoptSelectFrame`).
--- * Per-frame frame-state sync (formerly Element:update line ~2747 —
--- `Select.ensureFrameState`).
--- * Save/restore of select open/value/label (formerly the `select` branch of
--- Element:saveState / Element:restoreState).
---
--- Element retains `self._selectState` and `self.selectOption` for backward-
--- compat field access; runtime state lives ON THE ELEMENT. The behavior itself
--- is stateless + immutable (a single shared instance attaches to every
--- selectable element).
---
--- The 20 Element select-API delegate methods (openSelect, closeSelect,
--- toggleSelect, isSelectOpen, getSelectValue, setSelectValue, ...) stay as
--- 1-line forwarders into the Select module — the behavior owns the
--- *lifecycle* (attach / update / save / restore / detach), not the API
--- surface (per task 05 spec notes).
---
--- State ownership (per the locked Behavior contract):
--- * Per-element runtime state lives ON THE ELEMENT (self._selectState,
--- self.selectOption, self._selectParentElement, ...).
--- * The behavior instance is stateless + immutable and shared across elements.
--- * Element-class-level dependencies are resolved via `getmetatable(element)`
--- (which IS the Element class set by Element._construct), so the hook
--- signature stays exactly `(element, ...)` with no DI parameters.
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Resolve the Element class from an element instance.
--- `setmetatable({}, Element)` in `_construct` makes the instance metatable BE
--- the Element class, so this yields Element._Select, Element._Context,
--- Element._StateManager, etc. without threading deps through the hook signature.
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ============================================================================
--- shouldAttach (class-level predicate, no element required)
--- ============================================================================
-
--- Mirrors the cases that previously caused Element to initialize a Select
--- subsystem. An element owns select state exactly when it declares a
--- `selectParent` config (the dropdown trigger) or a `selectOption` config (an
--- option inside a dropdown). Checking the props (rather than the runtime
--- `_selectState`) lets shouldAttach run before onAttach initializes the
--- subsystem, matching the auto-attach contract established by Clickable /
--- TextEditable.
-local function shouldAttach(props)
- props = props or {}
- return type(props.selectParent) == "table" or type(props.selectOption) == "table"
-end
-
--- ============================================================================
--- onAttach — initialize the Select subsystem (formerly Element:_initSubSystems
--- lines ~810-825) and adopt the managed frame (formerly Element:_initPositioning
--- lines ~1700-1702).
--- ============================================================================
-
-local function onAttach(element)
- local Element = ElementClass(element)
-
- -- Initialize the appropriate select role. Mirrors the legacy _initSubSystems
- -- block exactly: selectParent → initSelectParent (sets _selectState +
- -- immediate-mode restore from StateManager); selectOption → initSelectOption
- -- (sets the option value/label/disabled).
- if type(element.selectParent) == "table" then
- Element._Select.initSelectParent(element, element.selectParent)
- end
-
- if type(element.selectOption) == "table" then
- Element._Select.initSelectOption(element, element.selectOption)
- end
-
- -- Adopt the managed dropdown frame. This was formerly the tail of
- -- _initPositioning (after the select parent's own addChild). It creates the
- -- select anchor, reparents the frame under it, and syncs visibility. Moving
- -- it here is safe because onAttach runs after _initPositioning: the parent's
- -- own positioning is finalized, so the anchor's geometry can be computed.
- if element._selectState and type(element.selectParent) == "table" and element.selectParent.selectFrame ~= nil then
- Element._Select.adoptSelectFrame(element, element.selectParent.selectFrame)
- end
-
- -- Backfill option registration for children added BEFORE this behavior
- -- attached. The auto-attach pass runs at the very end of Element.new
- -- (after _finalizeConstruction, which processes declarative `children`).
- -- Declarative select-option children are addChild'd to this element during
- -- _finalizeConstruction — at that point _selectState did not yet exist (this
- -- onAttach had not run), so their registerWithSelectParent call walked the
- -- parent chain, found no _selectState, and returned early. Re-scan now that
- -- _selectState is initialized so these options are registered + reparented
- -- into the managed frame exactly like runtime-added options.
- -- (registerWithSelectParent is idempotent — it skips options already
- -- registered — so this is a no-op for children added after _selectState was
- -- set, e.g. the common `FlexLove.new({ parent = sp, selectOption = {...} })`
- -- pattern.)
- if element._selectState then
- for _, child in ipairs(element.children) do
- if child.selectOption then
- Element._Select.registerWithSelectParent(child)
- Element._Select.attachOptionToManagedFrame(child)
- end
- end
- end
-end
-
-local function onDetach(element)
- -- Clear select-managed fields so the element can be GC'd cleanly in immediate
- -- mode (formerly part of Element:_cleanup). This mirrors the select-clearing
- -- block that lived in Element:_cleanup; Element:destroy separately routes
- -- through Select.cleanupDestroy for full teardown (idempotent with this).
- if element.selectParent then
- element.selectParent.onChange = nil
- end
- element._selectState = nil
- element._managedSelectOwner = nil
- element._managedSelectFrame = nil
- element._managedSelectAnchor = nil
- element._managedSelectBaseOpacity = nil
- element._managedSelectBaseVisibility = nil
- element._managedSelectBaseDisabled = nil
-end
-
--- ============================================================================
--- onUpdate — per-frame managed-frame layout sync (formerly Element:update
--- line ~2747 — `Select.ensureFrameState`).
--- ============================================================================
-
-local function onUpdate(element, dt)
- local Element = ElementClass(element)
- Element._Select.ensureFrameState(element)
-end
-
--- ============================================================================
--- onDraw — no-op.
--- ============================================================================
-
--- Select rendering is driven by the managed frame / anchor elements themselves
--- (visibility synced by Select.syncManagedFrameVisibility), not by the select
--- parent's draw path. The parent's own pixels are the theme/renderer's job.
-local function onDraw() end
-
--- ============================================================================
--- saveState / restoreState — select open/value/label (formerly the `select`
--- branch of Element:saveState / Element:restoreState).
--- ============================================================================
-
--- Returns a snapshot under the `select` key to match the legacy immediate-mode
--- restoreState contract (Element:restoreState looked up state.select). The
--- behavior-dispatch loop merges behavior snapshots into the top-level state
--- table, so returning { select = ... } slots in identically to the old inline
--- `state.select = selectState` assignment.
-local function saveState(element)
- local Element = ElementClass(element)
- local selectState = Element._Select.saveState(element)
- if selectState then
- return { select = selectState }
- end
- return nil
-end
-
--- Consumes the previously-saved snapshot keyed under `select`. The behavior-
--- dispatch loop passes the FULL top-level state table; this hook reads only
--- its own `state.select` slice, mirroring the legacy `if state.select then`
--- guard in Element:restoreState.
-local function restoreState(element, state)
- if not state then
- return
- end
- local Element = ElementClass(element)
- if state.select then
- Element._Select.restoreState(element, state.select)
- end
-end
-
--- ============================================================================
--- Build the (stateless, shared, immutable) behavior instance.
--- ============================================================================
-
-local Selectable = Behavior.new({
- onAttach = onAttach,
- onDetach = onDetach,
- onUpdate = onUpdate,
- onDraw = onDraw,
- saveState = saveState,
- restoreState = restoreState,
- shouldAttach = shouldAttach,
-})
-
--- Expose the predicate at module level so callers/tests can reference it
--- directly without an element instance (mirrors Behavior.shouldAttach).
-Selectable.shouldAttach = shouldAttach
-
-return Selectable
diff --git a/libs/flexlove/modules/behaviors/TextEditable.lua b/libs/flexlove/modules/behaviors/TextEditable.lua
deleted file mode 100644
index 7d3ee79b..00000000
--- a/libs/flexlove/modules/behaviors/TextEditable.lua
+++ /dev/null
@@ -1,576 +0,0 @@
--- modules/behaviors/TextEditable.lua
---
--- Concrete behavior: TextEditor subsystem ownership — text editing, cursor
--- management, text selection, text-related input handling, and text-editor
--- state save/restore.
---
--- This behavior consolidates the legacy `if self._textEditor` nil-guard
--- patterns that previously lived inside Element.lua:
---
--- * TextEditor creation + immediate-mode state restore (formerly
--- Element:_initSubSystems lines ~813-830 — the `if self.editable then
--- self._textEditor = Element._TextEditor.new {...}` block).
--- * Cursor-blink update (formerly Element:update line ~2810 —
--- `if self._textEditor then self._textEditor:update(self, dt) end`).
--- * The 27 text-editor delegate methods (formerly Element:setText /
--- getText / setCursorPosition / setSelection / focus / textinput /
--- keypressed / _handleTextClick / _handleTextDrag / ...). Each was a 3-line
--- nil-guard stub (check `_textEditor`, forward call, end). They are now
--- module-level functions on this behavior; Element retains only 1-line
--- forwarders that route through `Element._TextEditable.(self, ...)`.
--- * Text-editor state save/restore (formerly the textEditor branch of
--- Element:saveState / Element:restoreState), including the cursor/selection
--- field sync and the text-selection drag-tracking fields
--- (`_mouseDownPosition` / `_textDragOccurred`).
---
--- Element retains the `self._textEditor` field for backward-compat field
--- access (Renderer:drawText reads it directly for cursor/selection rendering);
--- runtime state lives ON THE ELEMENT. The behavior itself is stateless +
--- immutable + shared across elements.
---
--- State ownership (per the locked Behavior contract):
--- * Per-element runtime state lives ON THE ELEMENT (`self._textEditor`,
--- `self._mouseDownPosition`, `self._textDragOccurred`). The behavior
--- instance is stateless + immutable and shared across all editable
--- elements.
--- * Element-class-level dependencies are resolved via `getmetatable(element)`
--- (which IS the Element class set by Element._construct), so the hook
--- signature stays exactly `(element, ...)` with no DI parameters.
---
--- onDraw is a no-op: text/cursor/selection rendering stays in the Renderer's
--- command buffer (Layer 4 "text"), driven by the Thamed behavior's single
--- `Renderer:draw` call. The Renderer's `drawText` already reads
--- `element._textEditor` for cursor/selection, so TextEditable OWNS the
--- subsystem that drawText consumes, but the draw dispatch stays in the
--- renderer to preserve the unified transform/scissor command-buffer ordering
--- (mirrors Selectable.onDraw's no-op precedent, where rendering is owned by a
--- different layer). Hoisting drawText into this behavior's onDraw would
--- double-render text, since the Renderer command buffer already emits a "text"
--- layer for every element.
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Resolve the Element class from an element instance (mirrors Clickable /
--- Selectable). `setmetatable({}, Element)` in `_construct` makes the instance
--- metatable BE the Element class, so this yields Element._TextEditor,
--- Element._textEditorDeps, Element._Context, Element._StateManager, etc.
--- without threading deps through the hook signature.
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ============================================================================
--- shouldAttach (class-level predicate, no element required)
--- ============================================================================
-
--- Mirrors the spec predicate: attach when the element is text-editable OR
--- carries text content. onAttach only ALLOCATES a TextEditor when
--- `element.editable` is true (preserving the pre-refactor creation invariant
--- "TextEditor created iff editable"), so non-editable text labels attach the
--- behavior but allocate no TextEditor — their onUpdate/onDraw/saveState are
--- nil-guarded no-ops, and the Element forwarders route them through the
--- non-editable branch of each delegate function (reads/writes `element.text`
--- directly). This keeps shouldAttach faithful to the spec while preserving
--- exact pre-refactor allocation behavior.
-local function shouldAttach(props)
- props = props or {}
- return props.editable == true or props.text ~= nil
-end
-
--- ============================================================================
--- onAttach — create the TextEditor (formerly Element:_initSubSystems lines
--- ~813-830) and restore immediate-mode TextEditor state.
--- ============================================================================
-
-local function onAttach(element)
- local Element = ElementClass(element)
-
- -- Only editable elements own a TextEditor. Preserves the exact pre-refactor
- -- creation guard (`if self.editable then ... end`) — non-editable text
- -- elements attach the behavior (so their forwarders route through a single
- -- code path) but allocate no TextEditor.
- if not element.editable then
- return
- end
-
- -- Config is sourced from element fields (bound by _applyProps / _initVisualState
- -- before _attachBehaviors runs at the tail of Element.new) — NOT from raw
- -- props. The callbacks (onFocus/onBlur/onTextInput/onTextChange/onEnter) are
- -- schema-bound element fields by this point, and `element.text` is set by
- -- _initVisualState, so no `props` reference is needed here (the hook
- -- signature is `(element)`).
- element._textEditor = Element._TextEditor.new({
- editable = element.editable,
- multiline = element.multiline,
- passwordMode = element.passwordMode,
- textWrap = element.textWrap,
- maxLines = element.maxLines,
- maxLength = element.maxLength,
- placeholder = element.placeholder,
- inputType = element.inputType,
- textOverflow = element.textOverflow,
- scrollable = element.scrollable,
- autoGrow = element.autoGrow,
- selectOnFocus = element.selectOnFocus,
- cursorColor = element.cursorColor,
- selectionColor = element.selectionColor,
- cursorBlinkRate = element.cursorBlinkRate,
- text = element.text or "",
- onFocus = element.onFocus,
- onBlur = element.onBlur,
- onTextInput = element.onTextInput,
- onTextChange = element.onTextChange,
- onEnter = element.onEnter,
- }, Element._textEditorDeps)
-
- -- Restore TextEditor state from StateManager in immediate mode. Mirrors the
- -- legacy _initSubSystems immediate-mode restore. Safe to run here (after
- -- _construct registered the element with StateManager) — the StateManager
- -- lookup is sparse and returns nil for a fresh element. Mode-aware via
- -- Context.isImmediateMode (behavior-mode-unification task 11).
- if Element._Context.isImmediateMode() and element._stateId and element._stateId ~= "" then
- local state = Element._StateManager.getState(element._stateId)
- if state and state.textEditor then
- element._textEditor:setState(state.textEditor, element)
- end
- end
-end
-
-local function onDetach(element)
- -- Clear text-input callback closures read by TextEditor / KeyboardNavigation
- -- so the element's closure references can be collected in immediate mode
- -- (formerly part of Element:_cleanup). The TextEditor instance itself is
- -- INTENTIONALLY kept: Element:_cleanup preserves element structure for
- -- inspection (released when the element is GC'd).
- element.onTextInput = nil
- element.onTextChange = nil
- element.onEnter = nil
-end
-
--- ============================================================================
--- onUpdate — cursor-blink animation (formerly Element:update line ~2810).
--- ============================================================================
-
--- Drives TextEditor:update (cursor blink + blink-pause timer). Guarded on
--- `element._textEditor` because non-editable text elements attach this
--- behavior (per shouldAttach) but own no TextEditor. Element:update contains
--- zero text-editor references — the dispatch loop calls this hook.
-local function onUpdate(element, dt)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:update(element, dt)
- end
-end
-
--- ============================================================================
--- onDraw — no-op (see file header: text rendering stays in the Renderer
--- command buffer driven by the Thamed behavior's Renderer:draw call).
--- ============================================================================
-
-local function onDraw() end
-
--- ============================================================================
--- saveState / restoreState — TextEditor state + text-selection drag
--- tracking (formerly the textEditor branch of Element:saveState /
--- Element:restoreState, including the _mouseDownPosition / _textDragOccurred
--- fields).
--- ============================================================================
-
--- Returns a snapshot under the `textEditor` key to match the legacy immediate-
--- mode restoreState contract (Element:restoreState looked up state.textEditor).
--- The behavior-dispatch loop in Element:saveState merges behavior snapshots
--- into the top-level state table, so returning { textEditor = ... } slots in
--- identically to the old inline `state.textEditor = self._textEditor:getState()`
--- assignment. The drag-tracking fields are merged at the top level too
--- (matching the legacy `state._mouseDownPosition` / `state._textDragOccurred`
--- assignments) since they are text-selection state.
-local function saveState(element)
- local textEditor = element._textEditor
- if not textEditor then
- -- Non-editable text element: still persist drag-tracking fields if set
- -- (they are only ever set for editable elements, but persist defensively).
- local hasDragState = element._mouseDownPosition ~= nil or element._textDragOccurred ~= nil
- if not hasDragState then
- return nil
- end
- local snapshot = {}
- if element._mouseDownPosition ~= nil then
- snapshot._mouseDownPosition = element._mouseDownPosition
- end
- if element._textDragOccurred ~= nil then
- snapshot._textDragOccurred = element._textDragOccurred
- end
- return snapshot
- end
-
- local snapshot = { textEditor = textEditor:getState() }
- if element._mouseDownPosition ~= nil then
- snapshot._mouseDownPosition = element._mouseDownPosition
- end
- if element._textDragOccurred ~= nil then
- snapshot._textDragOccurred = element._textDragOccurred
- end
- return snapshot
-end
-
--- Consumes the previously-saved snapshot keyed under `textEditor` plus the
--- drag-tracking fields. The behavior-dispatch loop passes the FULL top-level
--- state table; this hook reads only its own slices, mirroring the legacy
--- `if self._textEditor and state.textEditor then ... end` guard.
-local function restoreState(element, state)
- if not state then
- return
- end
- local textEditor = element._textEditor
- if textEditor and state.textEditor then
- textEditor:setState(state.textEditor, element)
- -- Sync TextEditor's focus/cursor/selection state to Element for theme
- -- management (mirrors the legacy restoreState field sync).
- element._focused = textEditor._focused
- element._cursorPosition = textEditor._cursorPosition
- element._selectionStart = textEditor._selectionStart
- element._selectionEnd = textEditor._selectionEnd
- element._textBuffer = textEditor._textBuffer
- end
-
- -- Restore drag-tracking state for text selection (top-level keys).
- if state._mouseDownPosition ~= nil then
- element._mouseDownPosition = state._mouseDownPosition
- end
- if state._textDragOccurred ~= nil then
- element._textDragOccurred = state._textDragOccurred
- end
-end
-
--- ============================================================================
--- Text-editor delegate functions.
---
--- These are the module-level implementations of the 27 text-editor delegate
--- methods that previously lived on Element. Each mirrors the pre-refactor
--- Element method body VERBATIM (with `self` → `element`), including the
--- `element._textEditor` nil-guard: the guard is required because (a) non-
--- editable text elements attach this behavior (per shouldAttach) but own no
--- TextEditor, and (b) Element forwards these methods BEFORE onAttach has run
--- (e.g. an `onCreate` callback firing during _finalizeConstruction, which
--- runs before _attachBehaviors). The nil-guards live in THIS file (not in
--- Element.lua), so the Element.lua `if self._textEditor` count drops to 0.
---
--- Element retains 1-line forwarders: `Element.setText = function(self, text)
--- return Element._TextEditable.setText(self, text) end` (etc.), so external
--- callers (EventHandler, KeyboardNavigation, game UI) keep working unchanged.
---
--- The TextEditor API is mixed: most methods take the element as first arg
--- (`te:method(element, ...)` — "passesSelf"); a few getters omit it
--- (`te:method()`). The delegation contract is pinned by
--- subsystem_delegation_test.lua, so this mapping must match TextEditor's
--- method signatures exactly.
--- ============================================================================
-
--- --- Cursor management (passesSelf = element forwarded) ------------------
-
-local function setCursorPosition(element, position)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:setCursorPosition(element, position)
- end
-end
-
-local function getCursorPosition(element)
- local textEditor = element._textEditor
- if textEditor then
- return textEditor:getCursorPosition()
- end
- return 0
-end
-
-local function moveCursorBy(element, delta)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:moveCursorBy(element, delta)
- end
-end
-
-local function moveCursorToStart(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:moveCursorToStart(element)
- end
-end
-
-local function moveCursorToEnd(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:moveCursorToEnd(element)
- end
-end
-
-local function moveCursorToLineStart(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:moveCursorToLineStart(element)
- end
-end
-
-local function moveCursorToLineEnd(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:moveCursorToLineEnd(element)
- end
-end
-
-local function moveCursorToPreviousWord(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:moveCursorToPreviousWord(element)
- end
-end
-
-local function moveCursorToNextWord(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:moveCursorToNextWord(element)
- end
-end
-
--- --- Selection management ------------------------------------------------
-
-local function setSelection(element, startPos, endPos)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:setSelection(element, startPos, endPos)
- end
-end
-
-local function getSelection(element)
- local textEditor = element._textEditor
- if textEditor then
- return textEditor:getSelection()
- end
- return nil
-end
-
-local function hasSelection(element)
- local textEditor = element._textEditor
- if textEditor ~= nil then
- return textEditor:hasSelection()
- end
- return false
-end
-
-local function clearSelection(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:clearSelection(element)
- end
-end
-
-local function selectAll(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:selectAll(element)
- end
-end
-
-local function getSelectedText(element)
- local textEditor = element._textEditor
- if textEditor then
- return textEditor:getSelectedText()
- end
- return nil
-end
-
-local function deleteSelection(element)
- local textEditor = element._textEditor
- if textEditor then
- return textEditor:deleteSelection(element)
- end
- return false
-end
-
--- --- Focus management ----------------------------------------------------
-
-local function focus(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:focus(element)
- end
-end
-
-local function blur(element)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:blur(element)
- end
-end
-
-local function isFocused(element)
- local textEditor = element._textEditor
- if textEditor then
- return textEditor:isFocused()
- end
- return false
-end
-
--- --- Text buffer management (with post-delegation sync) ------------------
--- These methods sync `element.text` from the TextEditor result + drive
--- auto-grow, exactly as the legacy Element methods did.
-
-local function getText(element)
- local textEditor = element._textEditor
- if textEditor then
- return textEditor:getText()
- end
- return element.text or ""
-end
-
-local function setText(element, text)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:setText(element, text)
- element.text = textEditor:getText() -- Sync display text
- textEditor:updateAutoGrowHeight(element)
- return
- end
- element.text = text
-end
-
-local function insertText(element, text, position)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:insertText(element, text, position)
- element.text = textEditor:getText() -- Sync display text
- textEditor:updateAutoGrowHeight(element)
- end
-end
-
-local function deleteText(element, startPos, endPos)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:deleteText(element, startPos, endPos)
- element.text = textEditor:getText() -- Sync display text
- textEditor:updateAutoGrowHeight(element)
- end
-end
-
-local function replaceText(element, startPos, endPos, newText)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:replaceText(element, startPos, endPos, newText)
- element.text = textEditor:getText() -- Sync display text
- textEditor:updateAutoGrowHeight(element)
- end
-end
-
--- --- Mouse text selection ------------------------------------------------
-
-local function handleTextClick(element, mouseX, mouseY, clickCount)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:handleTextClick(element, mouseX, mouseY, clickCount)
- -- Store mouse down position on element for drag tracking
- if clickCount == 1 then
- element._mouseDownPosition = textEditor:mouseToTextPosition(element, mouseX, mouseY)
- end
- end
-end
-
-local function handleTextDrag(element, mouseX, mouseY)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:handleTextDrag(element, mouseX, mouseY)
- element._textDragOccurred = textEditor._textDragOccurred
- end
-end
-
--- --- Keyboard input ------------------------------------------------------
-
-local function textinput(element, text)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:handleTextInput(element, text)
- element.text = textEditor:getText() -- Sync display text
- textEditor:updateAutoGrowHeight(element)
- end
-end
-
-local function keypressed(element, key, scancode, isrepeat)
- local textEditor = element._textEditor
- if textEditor then
- textEditor:handleKeyPress(element, key, scancode, isrepeat)
- element.text = textEditor:getText() -- Sync display text
- textEditor:updateAutoGrowHeight(element)
- end
-end
-
--- ============================================================================
--- Build the (stateless, shared, immutable) behavior instance + thin module
--- table exposing the delegate functions (mirrors the Animated pattern).
--- ============================================================================
-
-local behavior = Behavior.new({
- onAttach = onAttach,
- onDetach = onDetach,
- onUpdate = onUpdate,
- onDraw = onDraw,
- saveState = saveState,
- restoreState = restoreState,
- shouldAttach = shouldAttach,
-})
-
--- Thin module table: exposes the frozen behavior instance (for the registry)
--- plus the text-editor delegate functions (for Element's 1-line forwarders).
--- All hooks delegate to the frozen behavior instance so dispatch sites get
--- the validated, frozen implementation. shouldAttach is also exposed at module
--- level (mirrors Clickable.shouldAttach) for tests/callers without an element.
-local TextEditable = {
- behavior = behavior,
- shouldAttach = shouldAttach,
- onAttach = onAttach,
- onUpdate = onUpdate,
- onDraw = onDraw,
- saveState = saveState,
- restoreState = restoreState,
- -- Text-editor delegate functions (Element forwarders route through these):
- setCursorPosition = setCursorPosition,
- getCursorPosition = getCursorPosition,
- moveCursorBy = moveCursorBy,
- moveCursorToStart = moveCursorToStart,
- moveCursorToEnd = moveCursorToEnd,
- moveCursorToLineStart = moveCursorToLineStart,
- moveCursorToLineEnd = moveCursorToLineEnd,
- moveCursorToPreviousWord = moveCursorToPreviousWord,
- moveCursorToNextWord = moveCursorToNextWord,
- setSelection = setSelection,
- getSelection = getSelection,
- hasSelection = hasSelection,
- clearSelection = clearSelection,
- selectAll = selectAll,
- getSelectedText = getSelectedText,
- deleteSelection = deleteSelection,
- focus = focus,
- blur = blur,
- isFocused = isFocused,
- getText = getText,
- setText = setText,
- insertText = insertText,
- deleteText = deleteText,
- replaceText = replaceText,
- _handleTextClick = handleTextClick,
- _handleTextDrag = handleTextDrag,
- textinput = textinput,
- keypressed = keypressed,
-}
-
--- Metatable so the module table itself satisfies the duck-typed registry
--- contract (iterating `Element._behaviorRegistry` calls `behavior.shouldAttach`
--- and `behavior.onAttach` / `behavior.onUpdate` directly). Falls through to the
--- frozen behavior instance for every hook / isBehavior parity.
-setmetatable(TextEditable, {
- __index = behavior,
- __tostring = function()
- return "TextEditable"
- end,
-})
-
-return TextEditable
diff --git a/libs/flexlove/modules/behaviors/Themed.lua b/libs/flexlove/modules/behaviors/Themed.lua
deleted file mode 100644
index bd431331..00000000
--- a/libs/flexlove/modules/behaviors/Themed.lua
+++ /dev/null
@@ -1,178 +0,0 @@
--- modules/behaviors/Themed.lua
---
--- Concrete behavior: Renderer ownership + theme-state rendering.
---
--- Themed owns the per-element Renderer instance and the single
--- `Renderer:draw` call that paints the core visual layers (background, image,
--- theme 9-patch, borders, text, customDraw). It is the behavior-mode-unification
--- replacement for the former `_initImageAndRenderer` Renderer creation block and
--- the former first `self._renderer:draw(self, backdropCanvas)` call in
--- Element:draw (behavior-mode-unification task 07).
---
--- Attachment rule (shouldAttach): every renderable Element. The pre-refactor
--- code unconditionally created a Renderer for every Element and unconditionally
--- called `Renderer:draw` in Element:draw; Themed mirrors that invariant so the
--- Renderer is always available to subsystems that depend on it (TextEditor font
--- / wrap delegation, ScrollManager scrollbar drawing) AND so visual rendering of
--- background / border / theme / image layers is preserved for every element.
--- Restricting attachment to `themeComponent`-only elements would break editable
--- text fields and scrollable containers (which need a Renderer for subsystem
--- delegation even when they have no theme component). The 9-patch theme-state
--- rendering within `Renderer:draw` is a no-op for elements without a
--- `themeComponent`, so always-attaching carries no rendering cost.
---
--- Themed and Imageable are paired (both configure the same `element._renderer`):
--- Themed.onAttach creates the Renderer with the theme/blur config; Imageable
--- (attached for imagePath/image elements) enriches the SAME renderer instance with
--- image config + deferred image loading. They share `element._renderer`.
---
--- onUpdate is a no-op: theme-state transitions are DRIVEN by the Clickable
--- behavior (whose onUpdate recomputes hover/press/focus and calls
--- `renderer:setThemeState`). Themed only READS that state for rendering, so it has
--- no per-frame update work.
---
--- saveState owns the blur-region snapshot (`state.blur`): the per-frame blur
--- geometry + radius/quality used by the Blur cache for invalidation (formerly
--- the inline `if self.backdropBlur or self.contentBlur` block of
--- Element:saveState — behavior-mode-unification task 12). restoreState is a
--- no-op: blur cache data is used for invalidation, not restoration (the Blur
--- cache is keyed by element id and cleared via `Blur.clearElementCache` from
--- FlexLove.endFrame, not replayed through restoreState).
---
--- State ownership (per the locked Behavior contract):
--- * Per-element runtime state lives ON THE ELEMENT (`element._renderer`,
--- `element._themeState`, `element.backdropBlur`, `element.contentBlur`).
--- The behavior instance is stateless and shared.
--- * `element._renderer` is recreated on attach; onDetach is a no-op — the
--- reference is released when the element is GC'd (Element:_cleanup keeps
--- element structure for inspection).
-
-local _pkg = (...):match("^(.-)behaviors%.") or "modules."
-local Behavior = require(_pkg .. "Behavior")
-
--- Resolve the Element class from an element instance (mirrors Clickable).
--- Element instances are created via `setmetatable({}, Element)`, so their
--- metatable IS the Element class — giving access to Element._Renderer,
--- Element._rendererDeps, etc. without threading deps through the hook signature.
-local function ElementClass(element)
- return getmetatable(element)
-end
-
--- ----------------------------------------------------------------------------
--- shouldAttach (class-level predicate, no element required)
--- ----------------------------------------------------------------------------
-
--- Returns true for every renderable Element. See file header for the rationale:
--- the pre-refactor invariant was "every Element has a Renderer; Element:draw
--- always calls Renderer:draw", and Thamed is the behavior-system embodiment of
--- that invariant. Returns true for `themeComponent`-bearing props (the spec's
--- headline case) and for every other element so subsystems/rendering stay intact.
-local function shouldAttach(props)
- return true
-end
-
--- ----------------------------------------------------------------------------
--- onAttach — create the Renderer with theme/blur config (formerly the
--- Renderer.new block of Element:_initImageAndRenderer).
--- ----------------------------------------------------------------------------
-
-local function onAttach(element)
- local Element = ElementClass(element)
-
- -- Create-or-reuse the Renderer. Thamed is the first render behavior in the
- -- registry, so it normally creates the instance; Imageable (if attached) will
- -- reuse this same instance for image config. Guarded so Imageable-onAttach-
- -- first (defensive) does not clobber an existing renderer.
- if element._renderer then
- return
- end
-
- -- NOTE: backgroundColor/borderColor/opacity/cornerRadius/themeComponent are
- -- intentionally NOT passed here. Renderer:draw() reads them from the element
- -- as the single source of truth (see Renderer.lua draw()). Only renderer-owned
- -- state (theme, blur) is cached on the renderer; image config is added by the
- -- Imageable behavior. border is element-sourced too.
- element._renderer = Element._Renderer.new({
- theme = element.theme,
- scaleCorners = element.scaleCorners,
- scalingAlgorithm = element.scalingAlgorithm,
- contentBlur = element.contentBlur,
- backdropBlur = element.backdropBlur,
- }, Element._rendererDeps)
-end
-
--- ----------------------------------------------------------------------------
--- onDraw — the single Renderer:draw call (formerly the first call in
--- Element:draw). Paints all core visual layers for this element.
--- ----------------------------------------------------------------------------
-
-local function onDraw(element, ctx)
- local renderer = element._renderer
- if not renderer then
- return
- end
- renderer:draw(element, ctx and ctx.backdropCanvas)
-end
-
--- ----------------------------------------------------------------------------
--- onDetach — no-op. Element:_cleanup preserves element structure for
--- inspection (the original invariant), so the Renderer reference is released
--- when the element is GC'd rather than torn down here. Present as an explicit
--- hook so the behavior conforms to the full lifecycle contract.
--- ----------------------------------------------------------------------------
-
-local function onDetach() end
-
--- ----------------------------------------------------------------------------
--- saveState — blur-region snapshot (formerly the `blur` branch of
--- Element:saveState). Returns `{ blur = {...} }` when the element configures a
--- backdrop or content blur, so the Blur cache can invalidate by element id;
--- nil otherwise. Mode-agnostic to match the legacy contract (the snapshot is
--- only read back by the cache-invalidation path, which itself is
--- immediate-mode-only via FlexLove.endFrame).
--- ----------------------------------------------------------------------------
-
-local function saveState(element)
- if not (element.backdropBlur or element.contentBlur) then
- return nil
- end
- local blur = {
- _blurX = element.x,
- _blurY = element.y,
- _blurWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right),
- _blurHeight = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom),
- }
- if element.backdropBlur then
- blur._backdropBlurRadius = element.backdropBlur.radius
- blur._backdropBlurQuality = element.backdropBlur.quality or 5
- end
- if element.contentBlur then
- blur._contentBlurRadius = element.contentBlur.radius
- blur._contentBlurQuality = element.contentBlur.quality or 5
- end
- return { blur = blur }
-end
-
--- restoreState — no-op: blur cache data is used for invalidation, not
--- restoration (see file header). Present so the behavior conforms to the
--- lifecycle contract without replaying geometry that the cache recomputes.
-
--- ----------------------------------------------------------------------------
--- Build the (stateless, shared, immutable) behavior instance.
--- ----------------------------------------------------------------------------
-
-local Themed = Behavior.new({
- onAttach = onAttach,
- onDetach = onDetach,
- onUpdate = function() end,
- onDraw = onDraw,
- saveState = saveState,
- restoreState = function() end,
-})
-
--- Expose the predicate at module level so callers/tests can reference it
--- directly without an element instance (mirrors Behavior.shouldAttach /
--- Clickable.shouldAttach).
-Themed.shouldAttach = shouldAttach
-
-return Themed
diff --git a/libs/flexlove/modules/types.lua b/libs/flexlove/modules/types.lua
deleted file mode 100644
index dd504797..00000000
--- a/libs/flexlove/modules/types.lua
+++ /dev/null
@@ -1,662 +0,0 @@
----@class SelectOptionProps
----@field value any -- Stable option value owned by the parent select
----@field label string? -- Optional label override, falls back to the element text
----@field disabled boolean? -- Whether the option can be selected
-local SelectOptionProps = {}
-
----@class SelectParentProps
----@field value any -- Currently selected option value
----@field open boolean? -- Initial open state for the select container
----@field placeholder string? -- Fallback text when no option is selected
----@field selectFrame Element? -- Optional pre-instantiated dropdown container; intended to be unattached before being adopted by the select
----@field onChange fun(element:Element, value:any, option:SelectOptionProps)? -- Called when selection changes
-local SelectParentProps = {}
-
----@class Animation
-local Animation = {}
-
----@class Color
-local Color = {}
-
----@class Theme
-local Theme = {}
-
----@class ThemeManager
-local ThemeManager = {}
-
---=====================================--
--- For Animation.lua
---=====================================--
----@alias EasingFunction fun(t:number): number
-
----@class AnimationProps
----@field duration number -- Duration in seconds
----@field start table -- Starting values (can contain: width, height, opacity, x, y, gap, imageOpacity, backgroundColor, borderColor, textColor, padding, margin, cornerRadius, transform, etc.)
----@field final table -- Final values (same properties as start)
----@field easing string? -- Easing function name: "linear", "easeInQuad", "easeOutQuad", "easeInOutQuad", "easeInCubic", "easeOutCubic", "easeInOutCubic", "easeInQuart", "easeOutQuart", "easeInExpo", "easeOutExpo" (default: "linear")
----@field keyframes AnimationKeyframe[]? -- Array of keyframes for complex animations
----@field onStart fun(animation:Animation, element:Element?)? -- Called when animation starts
----@field onUpdate fun(animation:Animation, element:Element?, progress:number)? -- Called each frame with progress (0-1)
----@field onComplete fun(animation:Animation, element:Element?)? -- Called when animation completes
----@field onCancel fun(animation:Animation, element:Element?)? -- Called when animation is cancelled
----@field transform TransformProps? -- Additional transform properties (legacy support)
----@field transition table? -- Transition properties (legacy support)
-local AnimationProps = {}
-
----@class Transform
----@field rotate number? Rotation in radians (default: 0)
----@field scaleX number? X-axis scale (default: 1)
----@field scaleY number? Y-axis scale (default: 1)
----@field translateX number? X translation in pixels (default: 0)
----@field translateY number? Y translation in pixels (default: 0)
----@field skewX number? X-axis skew in radians (default: 0)
----@field skewY number? Y-axis skew in radians (default: 0)
----@field originX number? Transform origin X (0-1, default: 0.5)
----@field originY number? Transform origin Y (0-1, default: 0.5)
-local Transform = {}
-
----@alias TransformProps Transform
-
----@class TransitionProps
----@field duration number?
----@field easing string?
----@field delay number?
----@field onComplete fun(element:Element)?
-
---=====================================--
--- For Element.lua
---=====================================--
----@class ElementProps
----@field id string? -- Unique identifier for the element (auto-generated in immediate mode if not provided)
----@field mode "immediate"|"retained"|nil -- Lifecycle mode override: "immediate" (auto-managed state), "retained" (manual state), nil (use global mode from FlexLove.getMode(), default)
----@field parent Element? -- Parent element for hierarchical structure
----@field x number|string|CalcObject? -- X coordinate: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0)
----@field y number|string|CalcObject? -- Y coordinate: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: 0)
----@field z number? -- Z-index for layering (default: 0, clamped to -999..999)
----@field tabIndex number? -- Tab navigation order: >0 (explicit order, visited first), 0 or nil (natural document order), -1 (excluded from keyboard navigation)
----@field width number|string|CalcObject? -- Width of the element: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: calculated automatically)
----@field height number|string|CalcObject? -- Height of the element: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: calculated automatically)
----@field minWidth number|string|CalcObject? -- Minimum width constraint: number (px), string ("50%", "10vw"), or CalcObject. Clamps both fixed `width` and the flex-distributed main size when horizontal.
----@field maxWidth number|string|CalcObject? -- Maximum width constraint: number (px), string ("50%", "10vw"), or CalcObject. Clamps both fixed `width` and the flex-distributed main size when horizontal.
----@field minHeight number|string|CalcObject? -- Minimum height constraint: number (px), string ("50%", "10vh"), or CalcObject. Clamps both fixed `height` and the flex-distributed main size when vertical.
----@field maxHeight number|string|CalcObject? -- Maximum height constraint: number (px), string ("50%", "10vh"), or CalcObject. Clamps both fixed `height` and the flex-distributed main size when vertical.
----@field top number|string|CalcObject? -- Offset from top edge: number (px), string ("50%", "10vh"), or CalcObject (CSS-style positioning)
----@field right number|string|CalcObject? -- Offset from right edge: number (px), string ("50%", "10vw"), or CalcObject (CSS-style positioning)
----@field bottom number|string|CalcObject? -- Offset from bottom edge: number (px), string ("50%", "10vh"), or CalcObject (CSS-style positioning)
----@field left number|string|CalcObject? -- Offset from left edge: number (px), string ("50%", "10vw"), or CalcObject (CSS-style positioning)
----@field border Border? -- Border configuration for the element
----@field borderColor Color? -- Color of the border (default: black)
----@field opacity number? -- Element opacity 0-1 (default: 1)
----@field visibility "visible"|"hidden"? -- Element visibility (default: "visible")
----@field display boolean? -- Whether element participates in layout, rendering, and hit testing (default: true). Set false for CSS display:none behavior (zero layout space, no rendering, no hit testing). NOTE: In retained mode, toggling at runtime requires setting the parent's `_dirty = true` or calling `layoutChildren()` on the parent to trigger re-layout.
----@field backgroundColor Color? -- Background color (default: transparent)
----@field cornerRadius number|{topLeft:number?, topRight:number?, bottomLeft:number?, bottomRight:number?}? -- Corner radius: number (all corners) or table for individual corners (default: 0)
----@field gap number|string|CalcObject? -- Space between children elements: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0)
----@field padding number|string|CalcObject|{top:number|string|CalcObject?, right:number|string|CalcObject?, bottom:number|string|CalcObject?, left:number|string|CalcObject?, horizontal:number|string|CalcObject?, vertical:number|string|CalcObject?}? -- Padding around children: single value, string, CalcObject for all sides, or table for individual sides (default: {top=0, right=0, bottom=0, left=0})
----@field margin number|string|CalcObject|{top:number|string|CalcObject?, right:number|string|CalcObject?, bottom:number|string|CalcObject?, left:number|string|CalcObject?, horizontal:number|string|CalcObject?, vertical:number|string|CalcObject?}? -- Margin around element: single value, string, CalcObject for all sides, or table for individual sides (default: {top=0, right=0, bottom=0, left=0})
----@field text string? -- Text content to display (default: nil)
----@field textAlign TextAlignSpec? -- Alignment of the text content: simple string, compound string ("top-left"), or {horizontal, vertical} table (default: START)
----@field textColor Color? -- Color of the text content (default: black or theme text color)
----@field textSize number|string? -- Font size: number (px), string with units ("2vh", "10%"), or preset ("xxs"|"xs"|"sm"|"md"|"lg"|"xl"|"xxl"|"3xl"|"4xl") (default: "md" or 12px)
----@field minTextSize number? -- Minimum text size in pixels for auto-scaling
----@field maxTextSize number? -- Maximum text size in pixels for auto-scaling
----@field fontFamily string? -- Font family name from theme or path to font file (default: theme default or system default, inherits from parent)
----@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true)
----@field positioning Positioning? -- Layout positioning mode: "absolute"|"relative"|"flex"|"grid" (default: RELATIVE)
----@field flexDirection FlexDirection? -- Direction of flex layout: "horizontal"|"vertical"|"row"|"column"|"row-reverse"|"column-reverse"|"horizontal-reverse"|"vertical-reverse" (row→horizontal, column→vertical, row-reverse→horizontal-reverse, column-reverse→vertical-reverse, default: HORIZONTAL)
----@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START)
----@field alignItems AlignItems? -- Alignment of items along cross axis (default: STRETCH)
----@field alignContent AlignContent? -- Alignment of lines in multi-line flex containers (default: STRETCH)
----@field flexWrap FlexWrap? -- Whether children wrap to multiple lines: "nowrap"|"wrap"|"wrap-reverse" (default: NOWRAP)
----@field flex number|string? -- Shorthand for flexGrow, flexShrink, flexBasis: number (flex-grow only), string ("1 0 auto"), or nil (default: nil)
----@field flexGrow number? -- How much the element should grow relative to siblings (default: 0)
----@field flexShrink number? -- How much the element should shrink relative to siblings (default: 1)
----@field flexBasis number|string|CalcObject? -- Initial size before growing/shrinking: number (px), string ("50%", "10vw", "auto"), or CalcObject (default: "auto")
----@field justifySelf JustifySelf? -- Alignment of the item itself along main axis (default: AUTO)
----@field alignSelf AlignSelf? -- Alignment of the item itself along cross axis (default: AUTO)
----@field onEvent fun(element:Element, event:InputEvent)? -- Callback function for interaction events
----@field onEventDeferred boolean? -- Whether onEvent callback should be deferred until after canvases are released (default: false)
----@field onFocus fun(element:Element)? -- Callback when element receives focus
----@field onFocusDeferred boolean? -- Whether onFocus callback should be deferred (default: false)
----@field dropFocusOnSelection boolean? -- Override keyboard-navigation focus drop after Enter/Space activation (default: nil, uses KeyboardNavigation.config.dropFocusOnSelection)
----@field onBlur fun(element:Element)? -- Callback when element loses focus
----@field onBlurDeferred boolean? -- Whether onBlur callback should be deferred (default: false)
----@field onTextInput fun(element:Element, text:string)? -- Callback when text is input
----@field onTextInputDeferred boolean? -- Whether onTextInput callback should be deferred (default: false)
----@field onTextChange fun(element:Element, text:string)? -- Callback when text content changes
----@field onTextChangeDeferred boolean? -- Whether onTextChange callback should be deferred (default: false)
----@field onEnter fun(element:Element)? -- Callback when Enter key is pressed
----@field onEnterDeferred boolean? -- Whether onEnter callback should be deferred (default: false)
----@field onCreate fun(element:Element, props:table)? -- Callback when element is created, receives the element and original creation props
----@field onCreateDeferred boolean? -- Whether onCreate callback should be deferred (default: false)
----@field onTouchEvent fun(element:Element, touchEvent:InputEvent)? -- Callback for touch-specific events (touchpress, touchmove, touchrelease)
----@field onTouchEventDeferred boolean? -- Whether onTouchEvent callback should be deferred (default: false)
----@field onGesture fun(element:Element, gesture:table)? -- Callback for recognized gestures (tap, swipe, pinch, etc.)
----@field onGestureDeferred boolean? -- Whether onGesture callback should be deferred (default: false)
----@field touchEnabled boolean? -- Whether the element responds to touch events (default: true)
----@field multiTouchEnabled boolean? -- Whether the element supports multiple simultaneous touches (default: false)
----@field transform TransformProps? -- Transform properties for animations and styling
----@field transition TransitionProps? -- Transition settings for animations
----@field customDraw fun(element:Element)? -- Custom rendering callback called after standard rendering but before visual feedback (default: nil)
----@field gridRows number|table? -- Number of equal 1fr rows, or array of track specs (e.g. {"1fr","100px","auto"})
----@field gridColumns number|table? -- Number of equal 1fr columns, or array of track specs (e.g. {"1fr","100px","auto"})
----@field columnGap number|string|CalcObject? -- Gap between grid columns: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0)
----@field rowGap number|string|CalcObject? -- Gap between grid rows: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: 0)
----@field theme string? -- Theme name to use (e.g., "space", "metal"). Defaults to theme from flexlove.init()
----@field themeComponent string? -- Theme component to use (e.g., "panel", "button", "input"). If nil, no theme is applied
----@field disabled boolean? -- Whether the element is disabled (default: false)
----@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
----@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false, or true when using themeComponent)
----@field themeStateLock boolean|string? -- Lock theme state: true/"default" = lock to base state, false = normal behavior, string = specific state ("hover", "pressed", "active", "disabled") (default: false)
----@field themeComponentDisabledStates string[]? -- List of theme states to suppress visually (e.g. {"hover", "pressed"}). Interaction logic still fires.
----@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Multiplier for auto-sized content dimensions (default: sourced from theme or {1, 1})
----@field scaleCorners number? -- Scale multiplier for 9-patch corners/edges. E.g., 2 = 2x size (overrides theme setting)
----@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-patch corners: "nearest" (sharp/pixelated) or "bilinear" (smooth) (overrides theme setting)
----@field contentBlur {radius:number, quality:number?}? -- Blur the element's content including children (radius: pixels, quality: 1-10, default(quality): 5)
----@field backdropBlur {radius:number, quality:number?}? -- Blur content behind the element (radius: pixels, quality: 1-10, default(quality): 5)
----@field editable boolean? -- Whether the element is editable (default: false)
----@field multiline boolean? -- Whether the element supports multiple lines (default: false)
----@field textWrap boolean|"word"|"char"? -- Text wrapping mode (default: false for single-line, "word" for multi-line)
----@field maxLines number? -- Maximum number of lines (default: nil)
----@field maxLength number? -- Maximum text length in characters (default: nil)
----@field placeholder string? -- Placeholder text when empty (default: nil)
----@field passwordMode boolean? -- Whether to display text as password (default: false, disables multiline)
----@field inputType "text"|"number"|"email"|"url"? -- Input type for validation (default: "text")
----@field textOverflow "clip"|"ellipsis"|"scroll"? -- Text overflow behavior (default: "clip")
----@field scrollable boolean? -- Whether text is scrollable (default: false for single-line, true for multi-line)
----@field autoGrow boolean? -- Whether element auto-grows with text (default: false for single-line, true for multi-line)
----@field selectOnFocus boolean? -- Whether to select all text on focus (default: false)
----@field cursorColor Color? -- Cursor color (default: nil, uses textColor)
----@field selectionColor Color? -- Selection background color (default: nil, uses theme or default)
----@field cursorBlinkRate number? -- Cursor blink rate in seconds (default: 0.5)
----@field selectParent SelectParentProps? -- Parent-owned select/dropdown state and callbacks
----@field selectOption SelectOptionProps? -- Option metadata attached to a child of a select parent
----@field overflow "visible"|"hidden"|"scroll"|"auto"? -- Overflow behavior (default: "hidden")
----@field overflowX "visible"|"hidden"|"scroll"|"auto"? -- X-axis overflow (overrides overflow)
----@field overflowY "visible"|"hidden"|"scroll"|"auto"? -- Y-axis overflow (overrides overflow)
----@field scrollbarWidth number? -- Width of scrollbar track in pixels (default: 12)
----@field scrollbarColor Color? -- Scrollbar thumb color (default: Color.new(0.5, 0.5, 0.5, 0.8))
----@field scrollbarTrackColor Color? -- Scrollbar track color (default: Color.new(0.2, 0.2, 0.2, 0.5))
----@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6)
----@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2)
----@field scrollSpeed number? -- Pixels per wheel notch (default: 20)
----@field invertScroll boolean? -- Invert mouse wheel scroll direction (default: false)
----@field smoothScrollEnabled boolean? -- Enable smooth scrolling animation for wheel events (default: false)
----@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars, default: uses first scrollbar or fallback rendering)
----@field scrollbarKnobOffset number|{x:number, y:number}|{horizontal:number, vertical:number}? -- Offset for scrollbar knob/handle position in pixels (number for both axes, or table for per-axis control, default: 0, adds to theme offset)
----@field scrollbarPlacement "reserve-space"|"overlay"? -- Scrollbar rendering mode: "reserve-space" (reduces content area, default) or "overlay" (renders over content)
----@field scrollbarBalance boolean? -- When true, reserve scrollbar space on both sides of content for visual balance (default: false)
----@field hideScrollbars boolean|{vertical:boolean, horizontal:boolean}? -- Hide scrollbars (boolean for both, or table for individual control, default: false)
----@field imagePath string? -- Path to image file (auto-loads via ImageCache)
----@field image love.Image? -- Image object to display
----@field objectFit "fill"|"contain"|"cover"|"scale-down"|"none"? -- Image fit mode (default: "fill")
----@field objectPosition string? -- Image position like "center center", "top left", "50% 50%" (default: "center center")
----@field imageOpacity number? -- Image opacity 0-1 (default: 1, combines with element opacity)
----@field imageRepeat "no-repeat"|"repeat"|"repeat-x"|"repeat-y"|"space"|"round"? -- Image repeat/tiling mode (default: "no-repeat")
----@field imageTint Color? -- Color to tint the image (default: nil/white, no tint)
----@field onImageLoad fun(element:Element, image:love.Image)? -- Callback when image loads successfully
----@field onImageLoadDeferred boolean? -- Whether onImageLoad callback should be deferred (default: false)
----@field onImageError fun(element:Element, error:string)? -- Callback when image fails to load
----@field onImageErrorDeferred boolean? -- Whether onImageError callback should be deferred (default: false)
----@field _scrollX number? -- Internal: scroll X position (restored in immediate mode)
----@field _scrollY number? -- Internal: scroll Y position (restored in immediate mode)
----@field children? ElementProps[]
----@field userdata table? -- User-defined data storage for custom properties
----@field ariaRole ARIA? -- ARIA role for screen readers (e.g., "button", "link", "dialog")
----@field ariaLabel string? -- Accessible name for screen readers (overrides text content)
----@field ariaDescribedBy string? -- ID of element that describes this element
----@field ariaExpanded boolean? -- Whether element is expanded/collapsed (for containers)
----@field ariaPressed boolean? -- Whether element is pressed (for toggle buttons)
----@field ariaChecked boolean? -- Whether element is checked (for checkboxes/radios)
----@field ariaDisabled boolean? -- Whether element is disabled (overrides disabled property)
----@field ariaBusy boolean? -- Whether element is processing (for live regions)
----@field ariaLive "off"|"polite"|"assertive"? -- Live region priority for announcements
-local ElementProps = {}
-
----@class Border
----@field top boolean|number -- true sets width to 1px, number sets width to specified pixels (default: 0)
----@field right boolean|number -- true sets width to 1px, number sets width to specified pixels (default: 0)
----@field bottom boolean|number -- true sets width to 1px, number sets width to specified pixels (default: 0)
----@field left boolean|number -- true sets width to 1px, number sets width to specified pixels (default: 0)
-local Border = {}
-
---=====================================--
--- For KeyboardNavigation.lua
---=====================================--
----@class KeyboardNavigationKeyConfig
----@field next string -- Key used to move to the next focusable element
----@field previous string -- Key used to move to the previous focusable element
----@field up string -- Key used for directional navigation upward
----@field down string -- Key used for directional navigation downward
----@field left string -- Key used for directional navigation leftward
----@field right string -- Key used for directional navigation rightward
----@field activate string[] -- Keys that activate the currently focused element
----@field dismiss string -- Key used to dismiss or clear the currently focused element
----@field toggleDebug string -- Key used to toggle keyboard-navigation debug tooling
----@field inspect string -- Key used to inspect the currently focused element in developer tools
-local KeyboardNavigationKeyConfig = {}
-
----@class KeyboardNavigationDeveloperToolsConfig
----@field enabled boolean? -- Enable keyboard-navigation developer tools (default: true)
----@field showProperties boolean? -- Show focused element properties in developer tools (default: true)
----@field highlightColor number[]? -- RGBA color used for keyboard-navigation debug highlighting (default: {1, 0.8, 0, 0.5})
-local KeyboardNavigationDeveloperToolsConfig = {}
-
----@class KeyboardNavigationFocusIndicatorConfig
----@field enabled boolean? -- Enable the keyboard focus indicator (default: true)
----@field color number[]? -- RGBA color of the focus indicator (default: {0.2, 0.6, 1.0, 0.8})
----@field lineWidth number? -- Focus indicator stroke width in pixels (default: 2)
----@field inset number? -- Offset from the element bounds in pixels (default: -3)
----@field borderRadius number? -- Focus indicator border radius in pixels (default: 4)
----@field animationDuration number? -- Focus indicator entrance animation duration in seconds (default: 0.15)
----@field pulseEnabled boolean? -- Enable pulse animation for the focus indicator when supported
----@field pulseDuration number? -- Seconds per pulse cycle
----@field pulseScaleMin number? -- Minimum scale during pulse animation
----@field pulseScaleMax number? -- Maximum scale during pulse animation
----@field draw fun(element:Element, bounds:table, style:KeyboardNavigationFocusIndicatorConfig)? -- Custom focus indicator renderer
-local KeyboardNavigationFocusIndicatorConfig = {}
-
----@class KeyboardNavigationConfig
----@field enabled boolean? -- Enable or disable keyboard navigation globally (default: true)
----@field debugMode boolean? -- Enable keyboard-navigation debug logging (default: false)
----@field keys KeyboardNavigationKeyConfig? -- Key bindings used by keyboard navigation
----@field wrapAround boolean? -- Allow wrapping from last to first focusable element (default: true)
----@field directionalNavigation boolean? -- Enable arrow-key directional navigation (default: true)
----@field focusVisible boolean? -- Show the focus indicator for keyboard-driven focus (default: true)
----@field autofocusOnCreate boolean? -- Auto-focus the first focusable element on creation (default: false)
----@field dropFocusOnSelection boolean? -- Drop focus after Enter/Space activates an element (default: true)
----@field developerTools KeyboardNavigationDeveloperToolsConfig? -- Developer tool settings for keyboard navigation
----@field focusIndicator KeyboardNavigationFocusIndicatorConfig? -- Focus indicator style configuration
-local KeyboardNavigationConfig = {}
-
---=====================================--
--- For FlexLove.init()
---=====================================--
----@class FlexLoveConfig
----@field baseScale {width:number?, height:number?}? -- Base resolution for responsive scaling (default: nil, no scaling)
----@field theme string|ThemeDefinition? -- Theme name (string) or ThemeDefinition to use (default: nil, no theme)
----@field immediateMode boolean? -- Enable immediate mode (React-like, recreates UI each frame) vs retained mode (default: false)
----@field autoFrameManagement boolean? -- Automatically call beginFrame/endFrame (default: false)
----@field stateRetentionFrames number? -- Number of frames to retain unused state in immediate mode (default: 60)
----@field maxStateEntries number? -- Maximum number of state entries before forcing cleanup (default: 1000)
----@field includeStackTrace boolean? -- Include stack traces in error messages (default: true)
----@field reportingLogLevel LOG_LEVEL? -- Error log level: 1: critical, 2: error, 3: warn, 4: info, 5: debug/all (default: 3:warn)
----@field errorLogTarget string? -- Error log target: "console", "file", "both" (default: "console")
----@field errorLogFile string? -- Path to error log file (default: "flexlove_errors.log")
----@field errorLogMaxSize number? -- Maximum error log file size in bytes (default: 1048576, 1MB)
----@field maxErrorLogFiles number? -- Maximum number of rotated error log files (default: 5)
----@field errorLogRotateEnabled boolean? -- Enable error log rotation (default: true)
----@field performanceMonitoring boolean? -- Enable performance monitoring (default: true)
----@field performanceHudKey string? -- Key to toggle performance HUD (default: "f3")
----@field performanceHudPosition {x:number, y:number}? -- Position of performance HUD (default: {x=10, y=10})
----@field performanceWarningThreshold number? -- Frame time warning threshold in ms (default: 13.0)
----@field performanceCriticalThreshold number? -- Frame time critical threshold in ms (default: 16.67)
----@field performanceLogToConsole boolean? -- Log performance metrics to console (default: false)
----@field performanceWarnings boolean? -- Enable performance warnings (default: false)
----@field memoryProfiling boolean? -- Enable memory profiling (default: false, auto-enabled in immediate mode)
----@field gcStrategy string? -- Garbage collection strategy: "auto", "periodic", "manual", "disabled" (default: "auto")
----@field gcMemoryThreshold number? -- Memory threshold in MB before forcing GC (default: 100)
----@field gcInterval number? -- Frames between GC steps in periodic mode (default: 60)
----@field gcStepSize number? -- Work units per GC step, higher = more aggressive (default: 200)
----@field immediateModeBlurOptimizations boolean? -- Cache blur canvases in immediate mode to avoid re-rendering each frame (default: true)
----@field keyboardNavigation boolean|KeyboardNavigationConfig? -- Enable keyboard navigation with defaults (`true`) or provide configuration overrides
----@field debugDraw boolean? -- Enable debug draw overlay showing element boundaries with random colors (default: false)
----@field debugDrawKey string? -- Key to toggle debug draw overlay at runtime (default: nil, no toggle key)
-local FlexLoveConfig = {}
-
---=====================================--
--- Public FlexLove API
---=====================================--
----@alias TextAlignCompound "top-left" | "top-center" | "top-right" | "center-left" | "center-center" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right"
----@alias TextAlignSpec TextAlign | TextAlignCompound | {horizontal: TextAlign, vertical: TextAlignVertical}
-
----@class FlexLoveEnums
----@field TextAlign TextAlign
----@field TextAlignVertical TextAlignVertical
----@field Positioning Positioning
----@field FlexDirection FlexDirection
----@field JustifyContent JustifyContent
----@field JustifySelf JustifySelf
----@field AlignItems AlignItems
----@field AlignSelf AlignSelf
----@field AlignContent AlignContent
----@field FlexWrap FlexWrap
----@field TextSize TextSize
----@field ImageRepeat ImageRepeat
----@field ARIA ARIA
-local FlexLoveEnums = {}
-
----@class AnimationKeyframe
----@field at number -- Normalized time position (0-1)
----@field values table -- Property values at this keyframe
----@field easing string|EasingFunction? -- Easing used between this and the next keyframe
-local AnimationKeyframe = {}
-
----@class AnimationGroupProps
----@field animations Animation[] -- Animations to coordinate
----@field mode "parallel"|"sequence"|"stagger"? -- Group playback mode (default: "parallel")
----@field stagger number? -- Delay between staggered animations in seconds (default: 0.1)
----@field onComplete fun(group:AnimationGroup)? -- Called when all animations complete
----@field onStart fun(group:AnimationGroup)? -- Called when the group starts
-local AnimationGroupProps = {}
-
----@class AnimationGroup
----@field animations Animation[]
----@field mode "parallel"|"sequence"|"stagger"
----@field stagger number
----@field onComplete fun(group:AnimationGroup)?
----@field onStart fun(group:AnimationGroup)?
-local AnimationGroup = {}
-
----@class Animation
----@field duration number
----@field start table
----@field final table
----@field elapsed number
----@field easing EasingFunction
----@field keyframes AnimationKeyframe[]?
----@field transform TransformProps?
----@field transition TransitionProps?
----@field onStart fun(animation:Animation, element:Element?)?
----@field onUpdate fun(animation:Animation, element:Element?, progress:number)?
----@field onComplete fun(animation:Animation, element:Element?)?
----@field onCancel fun(animation:Animation, element:Element?)?
----@field update fun(self:Animation, dt:number, element:table?): boolean
----@field findKeyframes fun(self:Animation, progress:number): AnimationKeyframe?, AnimationKeyframe?
----@field lerpKeyframes fun(self:Animation, prevFrame:AnimationKeyframe, nextFrame:AnimationKeyframe, easedT:number): table
----@field interpolate fun(self:Animation): table
----@field apply fun(self:Animation, element:table)
----@field pause fun(self:Animation)
----@field resume fun(self:Animation)
----@field isPaused fun(self:Animation): boolean
----@field reverse fun(self:Animation)
----@field isReversed fun(self:Animation): boolean
----@field setSpeed fun(self:Animation, speed:number)
----@field getSpeed fun(self:Animation): number
----@field seek fun(self:Animation, time:number)
----@field getState fun(self:Animation): string
----@field cancel fun(self:Animation, element:table?)
----@field reset fun(self:Animation)
----@field getProgress fun(self:Animation): number
----@field chain fun(self:Animation, nextAnimation:Animation|function): Animation
----@field delay fun(self:Animation, seconds:number): Animation
----@field repeatCount fun(self:Animation, count:number): Animation
----@field yoyo fun(self:Animation, enabled:boolean?): Animation
----@class AnimationModule
----@field Easing table -- Built-in easing functions and easing factories
----@field Transform table? -- Animation transform helpers exposed by the animation module
----@field Group AnimationGroup -- Animation group class table
----@field new fun(props:AnimationProps): Animation
----@field fade fun(duration:number, fromOpacity:number, toOpacity:number, easing:string?): Animation
----@field scale fun(duration:number, fromScale:{width:number, height:number}, toScale:{width:number, height:number}, easing:string?): Animation
----@field keyframes fun(props:{duration:number, keyframes:AnimationKeyframe[], onStart:function?, onUpdate:function?, onComplete:function?, onCancel:function?}): Animation
----@field chainSequence fun(animations:Animation[]): Animation
-local AnimationModule = {}
-
----@class ColorInputTable
----@field [1] number?
----@field [2] number?
----@field [3] number?
----@field [4] number?
----@field r number?
----@field g number?
----@field b number?
----@field a number?
-local ColorInputTable = {}
-
----@alias ColorInput string|Color|ColorInputTable
-
----@class ColorModule
----@field new fun(r:number?, g:number?, b:number?, a:number?): Color
----@field fromHex fun(hexWithTag:string): Color
----@field validateColorChannel fun(value:any, max:number?): boolean, number?
----@field validateHexColor fun(hex:string): boolean, string?
----@field validateRGBColor fun(r:number, g:number, b:number, a:number?, max:number?): boolean, string?
----@field isValidColorFormat fun(value:any): string?
----@field sanitizeColor fun(value:any, default:Color?): Color
----@field parse fun(value:any): Color
----@field lerp fun(colorA:Color, colorB:Color, t:number): Color
-local ColorModule = {}
-
----@class ThemeManagerConfig
----@field theme string? -- Theme name override
----@field themeComponent string? -- Component name to resolve from the theme
----@field disabled boolean? -- Force disabled theme state
----@field active boolean? -- Force active theme state
----@field disableHighlight boolean? -- Disable pressed highlight overlay
----@field themeStateLock boolean|string? -- Lock the theme state to base/default or a named state
----@field themeComponentDisabledStates string[]? -- List of theme states to suppress visually
----@field scaleCorners number? -- Scale multiplier for 9-patch corners and edges
----@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for non-stretched theme regions
-local ThemeManagerConfig = {}
-
----@class ThemeRegion
----@field x number
----@field y number
----@field w number
----@field h number
-local ThemeRegion = {}
-
----@class ThemeComponent
----@field atlas string|love.Image?
----@field insets {left:number, top:number, right:number, bottom:number}?
----@field regions {topLeft:ThemeRegion, topCenter:ThemeRegion, topRight:ThemeRegion, middleLeft:ThemeRegion, middleCenter:ThemeRegion, middleRight:ThemeRegion, bottomLeft:ThemeRegion, bottomCenter:ThemeRegion, bottomRight:ThemeRegion}?
----@field stretch {horizontal:table, vertical:table}?
----@field states table?
----@field contentAutoSizingMultiplier {width:number?, height:number?}?
----@field scaleCorners number?
----@field scalingAlgorithm "nearest"|"bilinear"?
----@field knobOffset number|{x:number, y:number}|{horizontal:number, vertical:number}?
-local ThemeComponent = {}
-
----@class ThemeDefinition
----@field name string
----@field atlas string|love.Image?
----@field components table
----@field scrollbars table?
----@field colors table?
----@field fonts table?
----@field contentAutoSizingMultiplier {width:number?, height:number?}?
-local ThemeDefinition = {}
-
----@class Theme
----@field name string
----@field atlas love.Image?
----@field atlasData love.ImageData?
----@field components table
----@field scrollbars table
----@field colors table
----@field fonts table
----@field contentAutoSizingMultiplier {width:number?, height:number?}?
----@class ThemeManager
----@field theme string?
----@field themeComponent string?
----@field disabled boolean
----@field active boolean
----@field disableHighlight boolean?
----@field themeStateLock boolean|string?
----@field themeComponentDisabledStates table
----@field scaleCorners number?
----@field scalingAlgorithm "nearest"|"bilinear"?
----@field updateState fun(self:ThemeManager, isHovered:boolean, isPressed:boolean, isFocused:boolean, isDisabled:boolean): string
----@field getState fun(self:ThemeManager): string
----@field setState fun(self:ThemeManager, state:string)
----@field hasThemeComponent fun(self:ThemeManager): boolean
----@field getTheme fun(self:ThemeManager): Theme?
----@field getComponent fun(self:ThemeManager): ThemeComponent?
----@field getStateComponent fun(self:ThemeManager): ThemeComponent?
----@field getScrollbarComponent fun(self:ThemeManager, scrollbarName:string?): ThemeComponent?
----@field getStyle fun(self:ThemeManager, property:string): any?
----@field _getScaledContentPaddingForState fun(self:ThemeManager, state:string, borderBoxWidth:number, borderBoxHeight:number): table?
----@field getScaledContentPaddingForState fun(self:ThemeManager, state:string, borderBoxWidth:number, borderBoxHeight:number): table? -- deprecated, use getScaledContentPadding
----@field getScaledContentPadding fun(self:ThemeManager, borderBoxWidth:number, borderBoxHeight:number): table?
----@field getContentAutoSizingMultiplier fun(self:ThemeManager): table?
----@field getDefaultFontFamily fun(self:ThemeManager): string?
----@field setTheme fun(self:ThemeManager, themeName:string?, componentName:string?)
----@field validateThemeStateLock fun(self:ThemeManager): boolean
----@class Color
----@field r number
----@field g number
----@field b number
----@field a number
----@field toRGBA fun(self:Color): number, number, number, number
----@class ThemeModule
----@field Manager ThemeManager -- Theme manager class table
----@field new fun(definition:ThemeDefinition): Theme
----@field load fun(path:string): Theme?
----@field setActive fun(themeOrName:string|Theme)
----@field getActive fun(): Theme?
----@field getComponent fun(componentName:string, state:string?): ThemeComponent?
----@field getDefaultScrollbar fun(): ThemeComponent?
----@field getScrollbar fun(scrollbarName:string, state:string?): ThemeComponent?
----@field getFont fun(fontName:string): string?
----@field getColor fun(colorName:string): Color?
----@field hasActive fun(): boolean
----@field getRegisteredThemes fun(): table
----@field getColorNames fun(): string[]
----@field getAllColors fun(): table
----@field getColorOrDefault fun(colorName:string, fallback:Color): Color
----@field get fun(themeName:string): Theme?
----@field validateTheme fun(theme:table?, options:table?): boolean, table
----@field sanitizeTheme fun(theme:table?): table
-local ThemeModule = {}
-
----@class FlexLove
----@field _VERSION string
----@field _DESCRIPTION string
----@field _URL string
----@field _LICENSE string
----@field Animation AnimationModule?
----@field Color ColorModule
----@field Theme ThemeModule?
----@field enums FlexLoveEnums
----@field isReady fun(): boolean
----@field init fun(config:FlexLoveConfig?)
----@field setKeyboardNavigationDebug fun(enabled:boolean)
----@field enableKeyboardNavigation fun(config:KeyboardNavigationConfig?)
----@field deferCallback fun(callback:function)
----@field executeDeferredCallbacks fun()
----@field resize fun()
----@field setMode fun(mode:"immediate"|"retained")
----@field getMode fun(): "immediate"|"retained"
----@field beginFrame fun()
----@field endFrame fun()
----@field draw fun(gameDrawFunc:function|nil, postDrawFunc:function|nil)
----@field getElementAtPosition fun(x:number, y:number): Element?
----@field update fun(dt:number)
----@field collectGarbage fun(mode:string?, stepSize:number?): number?
----@field setGCStrategy fun(strategy:"auto"|"periodic"|"manual"|"disabled")
----@field getGCStats fun(): GCStats
----@field textinput fun(text:string)
----@field keypressed fun(key:string, scancode:string, isrepeat:boolean)
----@field wheelmoved fun(dx:number, dy:number)
----@field touchpressed fun(id:lightuserdata, x:number, y:number, dx:number, dy:number, pressure:number)
----@field touchmoved fun(id:lightuserdata, x:number, y:number, dx:number, dy:number, pressure:number)
----@field touchreleased fun(id:lightuserdata, x:number, y:number, dx:number, dy:number, pressure:number)
----@field getActiveTouchCount fun(): number
----@field getTouchOwner fun(touchId:string): Element?
----@field getById fun(id:string): Element?
----@field destroy fun()
----@field new fun(props:ElementProps, callback:function?): Element?
----@field getStateCount fun(): number
----@field clearState fun(id:string)
----@field clearAllStates fun()
----@field getStateStats fun(): table
----@field calc fun(expr:string): CalcObject
----@field getFocusedElement fun(): Element?
----@field setFocusedElement fun(element:Element?)
----@field clearFocus fun()
----@field setDebugDraw fun(enabled:boolean)
----@field getDebugDraw fun(): boolean
-local FlexLove = {}
-
---=====================================--
--- For State Persistence
---=====================================--
----@class ElementStateData
----@field _focused boolean?
----@field eventHandler table? -- EventHandler state
----@field textEditor table? -- TextEditor state
----@field scrollManager table? -- ScrollManager state
----@field blur BlurCacheData? -- Blur cache invalidation data
-
----@class BlurCacheData
----@field _blurX number
----@field _blurY number
----@field _blurWidth number
----@field _blurHeight number
----@field _backdropBlurRadius number?
----@field _backdropBlurQuality number?
----@field _contentBlurRadius number?
----@field _contentBlurQuality number?
-
---=====================================--
--- For Calc.lua
---=====================================--
----@class CalcDependencies
----@field ErrorHandler ErrorHandler? -- Error handler module
-
----@class CalcToken
----@field type string -- Token type: "NUMBER", "UNIT", "PLUS", "MINUS", "MULTIPLY", "DIVIDE", "LPAREN", "RPAREN", "EOF"
----@field value number? -- Numeric value (for NUMBER tokens)
----@field unit string? -- Unit type: "px", "%", "vw", "vh" (for NUMBER tokens)
-
----@class CalcASTNode
----@field type string -- Node type: "number", "add", "subtract", "multiply", "divide"
----@field value number? -- Numeric value (for "number" nodes)
----@field unit string? -- Unit type (for "number" nodes)
----@field left CalcASTNode? -- Left operand (for operator nodes)
----@field right CalcASTNode? -- Right operand (for operator nodes)
-
----@class CalcObject
----@field _isCalc boolean -- Marker to identify calc objects (always true)
----@field _expr string -- Original expression string
----@field _ast CalcASTNode? -- Parsed abstract syntax tree (nil if parsing failed)
----@field _error string? -- Error message if parsing failed
-
---=====================================--
--- For FlexLove.lua Internals
---=====================================--
----@class GCConfig
----@field strategy string -- "auto", "periodic", "manual", or "disabled"
----@field memoryThreshold number -- MB before forcing GC
----@field interval number -- Frames between GC steps (for periodic mode)
----@field stepSize number -- Work units per GC step (higher = more aggressive)
-
----@class GCState
----@field framesSinceLastGC number -- Frames elapsed since last GC
----@field lastMemory number -- Last recorded memory usage in MB
----@field gcCount number -- Total number of GC operations performed
-
----@class GCStats
----@field gcCount number -- Total number of GC operations performed
----@field framesSinceLastGC number -- Frames elapsed since last GC
----@field currentMemoryMB number -- Current memory usage in MB
----@field strategy string -- Current GC strategy
----@field threshold number -- Memory threshold in MB
-
----@class FlexLoveDependencies
----@field Context table -- Context module
----@field Theme Theme? -- Theme module
----@field Color Color -- Color module
----@field Calc Calc -- Calc module
----@field Units table -- Units module
----@field Blur table? -- Blur module
----@field ImageRenderer table? -- ImageRenderer module
----@field ImageScaler table? -- ImageScaler module
----@field NinePatch table? -- NinePatch module
----@field RoundedRect table -- RoundedRect module
----@field ImageCache table? -- ImageCache module
----@field utils table -- Utils module
----@field Grid table -- Grid module
----@field InputEvent table -- InputEvent module
----@field GestureRecognizer table? -- GestureRecognizer module
----@field StateManager StateManager -- StateManager module
----@field TextEditor table -- TextEditor module
----@field LayoutEngine LayoutEngine -- LayoutEngine module
----@field Renderer table -- Renderer module
----@field EventHandler EventHandler -- EventHandler module
----@field ScrollManager table -- ScrollManager module
----@field ErrorHandler ErrorHandler -- ErrorHandler module
----@field Performance Performance? -- Performance module
----@field Transform table? -- Transform module
diff --git a/libs/flexlove/modules/utils.lua b/libs/flexlove/modules/utils.lua
deleted file mode 100644
index 71a074c0..00000000
--- a/libs/flexlove/modules/utils.lua
+++ /dev/null
@@ -1,319 +0,0 @@
-local modulePath = (...):match("(.-)[^%.]+$")
-local function req(name)
- return require(modulePath .. name)
-end
-
--- Focused sub-modules (utils now re-exports their surfaces as backward-compatible
--- aliases so call sites needn't change). Loaded eagerly so the aliases resolve.
-local NumberValidation = req("NumberValidation")
-local TextSanitizer = req("TextSanitizer")
-local PathValidator = req("PathValidator")
-local FontCache = req("FontCache")
-local Enums = req("Enums")
-
--- ErrorHandler is injected via init() (safeLoadImage closes over this upvalue).
-local ErrorHandler = nil
-
-local enums = Enums.enums
-
--- Generic math, table, and path helpers (utils' own concern).
--- All validation, font-cache, text-sanitization, and path-validation logic
--- lives in the focused sub-modules above and is re-exported below.
-
---- Get current keyboard modifiers state
----@return {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
-local function getModifiers()
- return {
- shift = love.keyboard.isDown("lshift", "rshift"),
- ctrl = love.keyboard.isDown("lctrl", "rctrl"),
- alt = love.keyboard.isDown("lalt", "ralt"),
- ---@diagnostic disable-next-line
- super = love.keyboard.isDown("lgui", "rgui"), -- cmd/windows key
- }
-end
-
-local TEXT_SIZE_PRESETS = {
- ["2xs"] = 0.75,
- xxs = 0.75,
- xs = 1.25,
- sm = 1.75,
- md = 2.25,
- lg = 2.75,
- xl = 3.5,
- xxl = 4.5,
- ["2xl"] = 4.5,
- ["3xl"] = 5.0,
- ["4xl"] = 7.0,
-}
-
---- Resolve text size preset to viewport units
----@param sizeValue string|number
----@return number?, string?
-local function resolveTextSizePreset(sizeValue)
- if type(sizeValue) == "string" then
- local preset = TEXT_SIZE_PRESETS[sizeValue]
- if preset then
- return preset, "vh"
- end
- end
- return nil, nil
-end
-
---- Auto-detect the base path where FlexLove is located
----@return string filesystemPath
-local function getFlexLoveBasePath()
- local info = debug.getinfo(1, "S")
- if info and info.source then
- local source = info.source
- if source:sub(1, 1) == "@" then
- source = source:sub(2)
- end
-
- local filesystemPath = source:match("(.*/)")
- if filesystemPath then
- local fsPath = filesystemPath
- fsPath = fsPath:gsub("^%./", "")
- fsPath = fsPath:gsub("/$", "")
- fsPath = fsPath:gsub("/modules$", "")
- return fsPath
- end
- end
- return "libs"
-end
-
-local FLEXLOVE_FILESYSTEM_PATH = getFlexLoveBasePath()
-
---- Helper function to resolve paths relative to FlexLove
----@param path string
----@return string
-local function resolveImagePath(path)
- if path:match("^/") or path:match("^[A-Z]:") then
- return path
- end
- return FLEXLOVE_FILESYSTEM_PATH .. "/" .. path
-end
-
--- Math utilities
-
---- Clamp a value between optional min/max bounds. Either bound may be nil.
---- When both bounds are inverted (min > max), max wins (matches CSS behavior).
----@param value number Value to clamp
----@param min number|nil Minimum value (nil = no lower bound)
----@param max number|nil Maximum value (nil = no upper bound)
----@return number Clamped value
-local function clamp(value, min, max)
- if min and value < min then
- value = min
- end
- if max and value > max then
- value = max
- end
- return value
-end
-
---- Linear interpolation between two values
----@param a number Start value
----@param b number End value
----@param t number Interpolation factor (0-1)
----@return number Interpolated value
-local function lerp(a, b, t)
- return a + (b - a) * t
-end
-
---- Round a number to the nearest integer
----@param value number Value to round
----@return number Rounded value
-local function round(value)
- return math.floor(value + 0.5)
-end
-
--- Image utilities
-
---- Safely load an image with error handling
---- Returns both Image and ImageData to avoid deprecated getData() API
----@param imagePath string Path to image file
----@return love.Image?, love.ImageData?, string? Returns image, imageData, or nil with error message
-local function safeLoadImage(imagePath)
- local success, imageData = pcall(function()
- return love.image.newImageData(imagePath)
- end)
-
- if not success then
- local errorMsg = string.format("Failed to load image data: %s - %s", imagePath, tostring(imageData))
- if ErrorHandler then
- ErrorHandler:warn("utils", "RES_004", {
- resourceType = "image data",
- path = imagePath,
- error = tostring(imageData),
- })
- end
- return nil, nil, errorMsg
- end
-
- local imageSuccess, image = pcall(function()
- return love.graphics.newImage(imageData)
- end)
-
- if imageSuccess then
- return image, imageData, nil
- else
- local errorMsg = string.format("Failed to create image: %s - %s", imagePath, tostring(image))
- if ErrorHandler then
- ErrorHandler:warn("utils", "RES_004", {
- resourceType = "image",
- path = imagePath,
- error = tostring(image),
- })
- end
- return nil, nil, errorMsg
- end
-end
-
--- Color manipulation utilities
-
---- Brighten a color by a factor
----@param r number Red component (0-1)
----@param g number Green component (0-1)
----@param b number Blue component (0-1)
----@param a number Alpha component (0-1)
----@param factor number Brightness factor (e.g., 1.2 for 20% brighter)
----@return number, number, number, number Brightened color components
-local function brightenColor(r, g, b, a, factor)
- return math.min(1, r * factor), math.min(1, g * factor), math.min(1, b * factor), a
-end
-
--- Property normalization utilities
-
---- Normalize a boolean or table property with vertical/horizontal fields
----@param value boolean|table|nil Input value (boolean applies to both, table for individual control)
----@param defaultValue boolean Default value if nil (default: false)
----@return table Normalized table with vertical and horizontal fields
-local function normalizeBooleanTable(value, defaultValue)
- defaultValue = defaultValue or false
-
- if value == nil then
- return { vertical = defaultValue, horizontal = defaultValue }
- end
-
- if type(value) == "boolean" then
- return { vertical = value, horizontal = value }
- end
-
- if type(value) == "table" then
- return {
- vertical = value.vertical ~= nil and value.vertical or defaultValue,
- horizontal = value.horizontal ~= nil and value.horizontal or defaultValue,
- }
- end
-
- return { vertical = defaultValue, horizontal = defaultValue }
-end
-
---- Normalize an offset value to {x, y} or {horizontal, vertical} format
----@param value number|table|nil Input value (number applies to both, table for individual control)
----@param defaultValue number Default value if nil (default: 0)
----@return table Normalized table with x/y or horizontal/vertical fields
-local function normalizeOffsetTable(value, defaultValue)
- defaultValue = defaultValue or 0
-
- if value == nil then
- return { x = defaultValue, y = defaultValue, horizontal = defaultValue, vertical = defaultValue }
- end
-
- if type(value) == "number" then
- return { x = value, y = value, horizontal = value, vertical = value }
- end
-
- if type(value) == "table" then
- -- Support both {x, y} and {horizontal, vertical} formats
- local x = value.x or value.horizontal or defaultValue
- local y = value.y or value.vertical or defaultValue
- return {
- x = x,
- y = y,
- horizontal = x,
- vertical = y,
- }
- end
-
- return { x = defaultValue, y = defaultValue, horizontal = defaultValue, vertical = defaultValue }
-end
-
---- Apply content auto-sizing multiplier to a dimension
----@param value number The dimension value
----@param multiplier table? The contentAutoSizingMultiplier table {width:number?, height:number?}
----@param axis "width"|"height" Which axis to apply
----@return number The multiplied value
-local function applyContentMultiplier(value, multiplier, axis)
- if multiplier and multiplier[axis] then
- return value * multiplier[axis]
- end
- return value
-end
-
---- Initialize dependencies
----@param deps table Dependencies: { ErrorHandler = ErrorHandler }
-local function init(deps)
- if type(deps) == "table" then
- ErrorHandler = deps.ErrorHandler
- end
- -- Propagate shared ErrorHandler to focused sub-modules that need it.
- NumberValidation.init({ ErrorHandler = ErrorHandler, clamp = clamp })
- TextSanitizer.init({ ErrorHandler = ErrorHandler })
- FontCache.init({ ErrorHandler = ErrorHandler, resolveImagePath = resolveImagePath })
- -- PathValidator has no external dependencies.
-end
-
-return {
- enums = enums,
- FONT_CACHE = FontCache.FONT_CACHE,
- resolveTextSizePreset = resolveTextSizePreset,
- getModifiers = getModifiers,
- TEXT_SIZE_PRESETS = TEXT_SIZE_PRESETS,
- init = init,
- clamp = clamp,
- -- Alias for `clamp`; exposed under the size-clamping name so Element/LayoutEngine
- -- and tests can reference min/max content-size clamping explicitly.
- clampSize = clamp,
- lerp = lerp,
- round = round,
- safeLoadImage = safeLoadImage,
- brightenColor = brightenColor,
- resolveImagePath = resolveImagePath,
- normalizeBooleanTable = normalizeBooleanTable,
- normalizeOffsetTable = normalizeOffsetTable,
- applyContentMultiplier = applyContentMultiplier,
- -- Backward-compatible aliases (delegated to focused sub-modules)
- validateEnum = NumberValidation.validateEnum,
- validateRange = NumberValidation.validateRange,
- validateType = NumberValidation.validateType,
- isNaN = NumberValidation.isNaN,
- isInfinity = NumberValidation.isInfinity,
- validateNumber = NumberValidation.validateNumber,
- sanitizeNumber = NumberValidation.sanitizeNumber,
- validateInteger = NumberValidation.validateInteger,
- validatePercentage = NumberValidation.validatePercentage,
- validateOpacity = NumberValidation.validateOpacity,
- validateDegrees = NumberValidation.validateDegrees,
- validateCoordinate = NumberValidation.validateCoordinate,
- validateDimension = NumberValidation.validateDimension,
- normalizePath = PathValidator.normalizePath,
- sanitizePath = PathValidator.sanitizePath,
- isPathSafe = PathValidator.isPathSafe,
- validatePath = PathValidator.validatePath,
- getFileExtension = PathValidator.getFileExtension,
- hasAllowedExtension = PathValidator.hasAllowedExtension,
- sanitizeText = TextSanitizer.sanitizeText,
- validateTextInput = TextSanitizer.validateTextInput,
- validateTextRange = TextSanitizer.validateTextRange,
- escapeHtml = TextSanitizer.escapeHtml,
- escapeLuaPattern = TextSanitizer.escapeLuaPattern,
- stripNonPrintable = TextSanitizer.stripNonPrintable,
- resolveFontPath = FontCache.resolveFontPath,
- getFont = FontCache.getFont,
- getFontCacheStats = FontCache.getFontCacheStats,
- setFontCacheSize = FontCache.setFontCacheSize,
- clearFontCache = FontCache.clearFontCache,
- preloadFont = FontCache.preloadFont,
- resetFontCacheStats = FontCache.resetFontCacheStats,
-}
diff --git a/main.lua b/main.lua
index e1937fca..f8c349e8 100644
--- a/main.lua
+++ b/main.lua
@@ -11,7 +11,9 @@
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
+local LaunchOptions = require("src.core.LaunchOptions")
local NxDisplay = require("src.core.NxDisplay")
+local PlatformHooks = require("src.core.PlatformHooks")
-- Lua errors: persist a redacted trace in the save dir and surface a hint.
do
@@ -29,6 +31,21 @@ end
local Game, EditorApp, Importer, TouchEditor
+-- #887: quit-to-launcher state, shared by love.load and love.quit (both need
+-- it, so it is declared here rather than next to love.quit).
+-- * launchedIntoGame -- a --game / POKEPORT_GAME shortcut booted this
+-- session straight into a game, so there is no launcher behind it and a
+-- window close must exit. Restarting instead re-read the same shortcut
+-- and came right back into the game, and the next close did it again:
+-- the app could not be closed at all (macOS feels this worst, where the
+-- red X, Cmd+Q and the Dock's Quit are all the same quit event).
+-- * RELAUNCH_MARKER -- written in the save dir just before the #785
+-- restart, so the fresh boot ignores any boot-straight-into-a-game
+-- option exactly once and keeps #785's promise of landing in the
+-- launcher, whatever put the game on screen this time.
+local launchedIntoGame = false
+local RELAUNCH_MARKER = "relaunch_to_launcher.txt"
+
local autopilot -- optional scripted-input dev tool (tests/autopilot.lua)
local driverCo -- optional frame-driver (POKEPORT_DRIVER=file.lua): a
-- coroutine that receives `Game` and yields once per
@@ -313,6 +330,47 @@ function love.load(args)
return
end
+ -- The launcher draws before any game boots, so the mod loader has not run
+ -- and Strings has no catalog. Routing the launcher's text through Strings
+ -- (#767) only pays off if something fills that catalog this early, and no
+ -- restart could: the ordering is the same on every launch. Read the
+ -- enabled mods' string catalogs -- data only, no entry chunk -- so a
+ -- translation reaches the launcher too. Game:load replaces this with the
+ -- real merged catalog once a version boots.
+ do
+ local preload = require("src.mods.LauncherMods").translationStrings()
+ if preload then require("src.core.Strings").load({ strings = preload }) end
+ end
+
+ -- LAUNCH OPTIONS: skip the launcher and boot a game directly.
+ -- --game red|blue|yellow (or POKEPORT_GAME / POKEPORT_LAUNCH)
+ -- --slot optional; picks the save slot to load
+ -- --launcher force the launcher even if a game is set
+ -- This is what a desktop shortcut, a Steam entry, or a frontend like
+ -- EmulationStation needs: one click into the game the player wants, with no
+ -- menu in between. A game that is not imported falls through to the
+ -- launcher on its tab rather than booting into nothing.
+ -- A window close that restarted us into the launcher (#785) leaves the
+ -- marker behind: consume it and stay on the launcher, or the shortcut below
+ -- would boot the same game again and that close would restart again,
+ -- forever (#887). Consumed on read, so the very next launch is normal.
+ local relaunched = love.filesystem.getInfo(RELAUNCH_MARKER) ~= nil
+ if relaunched then pcall(love.filesystem.remove, RELAUNCH_MARKER) end
+
+ local launchGame, launchSlot = LaunchOptions.resolve(arg)
+ if launchGame and not relaunched and not LaunchOptions.forceLauncher(arg) then
+ if RomImporter.isReady(launchGame) then
+ if launchSlot then LaunchOptions.selectSlot(launchGame, launchSlot) end
+ -- No launcher behind this session: love.quit must exit, not restart.
+ launchedIntoGame = true
+ bootGame(launchGame)
+ return
+ end
+ -- Not importable yet: open the launcher already showing that game, so the
+ -- shortcut still lands the player where they meant to go.
+ LaunchOptions.pendingTab = launchGame
+ end
+
-- Interactive: the launcher always runs. Red, Blue, and Yellow are each
-- live: a column shows Play when that game's ROM is already imported, or
-- Choose ROM / drag-drop when it is not. Any dropped .gb is routed by its
@@ -369,7 +427,11 @@ function love.update(dt)
end
return
end
- Game:update(dt)
+ -- Mods may wrap or veto the per-frame simulation step (pause it, react
+ -- to external platform state, etc.) -- see docs/modding.md's core.update
+ -- entry. Vanilla behavior (used when no mod claims the hook) is just
+ -- Game:update(dt), unconditionally, exactly as before this hook existed.
+ PlatformHooks.update(Game, dt)
end
function love.draw()
@@ -597,7 +659,7 @@ function love.touchpressed(id, x, y, dx, dy, pressure)
-- Android's synthesized mouse twin so Import cannot double-fire (#553).
return Importer:touchpressed(id, x, y, dx, dy, pressure)
end
- Game:touchpressed(id, x, y)
+ Game:touchpressed(id, x, y, dx, dy, pressure)
end
function love.touchmoved(id, x, y, dx, dy, pressure)
@@ -609,7 +671,7 @@ function love.touchmoved(id, x, y, dx, dy, pressure)
if Importer then
return Importer:touchmoved(id, x, y, dx, dy, pressure)
end
- Game:touchmoved(id, x, y)
+ Game:touchmoved(id, x, y, dx, dy, pressure)
end
function love.touchreleased(id, x, y, dx, dy, pressure)
@@ -621,7 +683,7 @@ function love.touchreleased(id, x, y, dx, dy, pressure)
if Importer then
return Importer:touchreleased(id, x, y, dx, dy, pressure)
end
- Game:touchreleased(id, x, y)
+ Game:touchreleased(id, x, y, dx, dy, pressure)
end
function love.wheelmoved(x, y)
@@ -634,7 +696,33 @@ function love.wheelmoved(x, y)
Game:wheelmoved(x, y)
end
+-- #781: Linux X11 multi-monitor with the primary display away from desktop
+-- (0,0): SDL's polled mouse state can come back in desktop-virtual
+-- coordinates while the event stream stays window-relative, which strands
+-- every polled consumer (launcher Kit rising-edge clicks, the pad-cursor
+-- motion yield, PadCursor) on coordinates no hit test can match. Sanitize
+-- the poll once here: remember the last window-relative event coordinates
+-- and substitute them whenever the polled value falls outside the window.
+-- Linux only -- macOS / Windows / mobile keep the stock function, and the
+-- NX launcher shim still composes because it captures whatever
+-- love.mouse.getPosition is at bridge time (_ensureNxPointerBridge).
+local eventMouseX, eventMouseY
+if love.system and love.system.getOS() == "Linux"
+ and love.mouse and love.mouse.getPosition then
+ local polledGetPosition = love.mouse.getPosition
+ love.mouse.getPosition = function()
+ local x, y = polledGetPosition()
+ local w, h = love.graphics.getDimensions()
+ if x < 0 or y < 0 or x > w or y > h then
+ if eventMouseX then return eventMouseX, eventMouseY end
+ return math.max(0, math.min(x, w)), math.max(0, math.min(y, h))
+ end
+ return x, y
+ end
+end
+
function love.mousepressed(x, y, button, istouch)
+ if not istouch then eventMouseX, eventMouseY = x, y end
if TouchEditor then
-- Android primary touch already arrived via love.touchpressed; a second
-- mouse path would double-fire Done / begin a second drag.
@@ -658,12 +746,19 @@ function love.mousepressed(x, y, button, istouch)
if istouch and love.system.getOS() == "Android" then return end
return EditorApp.mousepressed(x, y, button)
end
- if mouseTouch and Game and button == 1 then
- Game:touchpressed("mouse", x, y)
+ if mouseTouch then
+ -- the mouse is standing in for a finger: the touch path owns it, and
+ -- feeding the same press back in as a mouse pointer would double it
+ if Game and button == 1 then Game:touchpressed("mouse", x, y) end
+ return
end
+ -- #807: a real mouse reaches gameplay as a pointer event for mods; Game
+ -- drops synthesized istouch twins so a mobile touch that already arrived
+ -- through love.touchpressed cannot fire twice
+ if Game then Game:mousepressed(x, y, button, istouch) end
end
-function love.mousereleased(x, y, button)
+function love.mousereleased(x, y, button, istouch)
if TouchEditor then
if love.system.getOS() == "Android" then return end
return TouchEditor.mousereleased(x, y, button)
@@ -672,20 +767,25 @@ function love.mousereleased(x, y, button)
if editorMode and EditorApp.mousereleased then
return EditorApp.mousereleased(x, y, button)
end
- if mouseTouch and Game and button == 1 then
- Game:touchreleased("mouse", x, y)
+ if mouseTouch then
+ if Game and button == 1 then Game:touchreleased("mouse", x, y) end
+ return
end
+ if Game then Game:mousereleased(x, y, button, istouch) end
end
-function love.mousemoved(x, y)
+function love.mousemoved(x, y, dx, dy, istouch)
+ if not istouch then eventMouseX, eventMouseY = x, y end
if TouchEditor then
if love.system.getOS() == "Android" then return end
return TouchEditor.mousemoved(x, y)
end
if editorMode or Importer then return end
- if mouseTouch and Game and love.mouse.isDown(1) then
- Game:touchmoved("mouse", x, y)
+ if mouseTouch then
+ if Game and love.mouse.isDown(1) then Game:touchmoved("mouse", x, y) end
+ return
end
+ if Game then Game:mousemoved(x, y, dx, dy, istouch) end
end
function love.textinput(text)
@@ -696,9 +796,51 @@ function love.textinput(text)
end
end
+-- #785: set once love.quit has routed a window close into HostShell.restart,
+-- so the follow-up quit event the restart itself raises (quit("restart") on
+-- desktop; AppImage and Android relaunch the process instead, #575) falls
+-- through to the normal shutdown below instead of restarting forever.
+local quitToLauncher = false
+
function love.quit()
if editorMode and EditorApp.quit then
- return EditorApp.quit() -- return true to abort quit
+ -- true blocks the quit (unsaved-changes prompt). A quit that proceeds
+ -- must fall through to the worker shutdowns below instead of returning:
+ -- the bundled editor opens from a live launcher whose update-check and
+ -- fetch-pool workers are still parked in Channel:demand(), and returning
+ -- here skipped their "quit" push, so the process outlived the closed
+ -- window and kept the install folder locked on Windows (#727).
+ if EditorApp.quit() then return true end
+ end
+ -- Closing the window of a running game returns to the launcher instead of
+ -- exiting the app, so testing a mod does not need a relaunch every time
+ -- (#785). Game is only non-nil once bootGame ran; Importer non-nil means
+ -- the launcher (or its import) owns the window and its close still quits.
+ -- Scripted and headless runs (autopilot, frame driver, import-only, ROM
+ -- path import) keep the plain exit so they terminate as before. Nothing
+ -- is saved here on purpose: a window close never wrote the save, and the
+ -- restart path must be no worse than that, not quietly better.
+ local scripted = os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER")
+ or os.getenv("POKEPORT_IMPORT_ONLY") == "1" or os.getenv("POKEPORT_IMPORT_ROM")
+ -- #887: a shortcut session (--game / POKEPORT_GAME) has no launcher to go
+ -- back to and the restart would re-read the shortcut, so it exits instead.
+ --
+ -- A platform launcher that owns "return to launcher" itself (see
+ -- docs/modding.md's core.quit_to_launcher entry) may veto returning to
+ -- this Lua launcher via that hook. Vanilla behavior (used when no mod
+ -- claims the hook) is exactly the condition below.
+ local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
+ return Game and not Importer and not quitToLauncher and not scripted
+ and not launchedIntoGame
+ end)
+ if wouldReturnToLauncher then
+ quitToLauncher = true
+ -- Tell the fresh boot to ignore any boot-straight-into-a-game option this
+ -- once, so the restart really does land in the launcher (#887). A failed
+ -- write only costs that suppression, so it must never block the restart.
+ pcall(love.filesystem.write, RELAUNCH_MARKER, "1")
+ require("src.core.HostShell").restart()
+ return true -- abort this quit; the restart lands back in the launcher
end
pcall(function()
require("src.core.DiscordPresence").shutdown()
@@ -713,6 +855,12 @@ function love.quit()
if package.loaded["src.update.Check"] then
pcall(package.loaded["src.update.Check"].shutdown)
end
+ -- The launcher's fetch pool is the same story: its workers idle in
+ -- Channel:demand(), which never returns on its own, so a launcher that ever
+ -- touched the network would hang the process on exit (#339's shape again).
+ if package.loaded["src.net.Fetch"] then
+ pcall(package.loaded["src.net.Fetch"].shutdown)
+ end
end
function love.filedropped(file)
diff --git a/mobile/android/app/src/main/res/drawable-hdpi/love.png b/mobile/android/app/src/main/res/drawable-hdpi/love.png
index da3ef3a6..b7dcebb4 100644
Binary files a/mobile/android/app/src/main/res/drawable-hdpi/love.png and b/mobile/android/app/src/main/res/drawable-hdpi/love.png differ
diff --git a/mobile/android/app/src/main/res/drawable-mdpi/love.png b/mobile/android/app/src/main/res/drawable-mdpi/love.png
index 82caa826..57a1055d 100644
Binary files a/mobile/android/app/src/main/res/drawable-mdpi/love.png and b/mobile/android/app/src/main/res/drawable-mdpi/love.png differ
diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/love.png b/mobile/android/app/src/main/res/drawable-xhdpi/love.png
index b2e6d1a2..7336e7ea 100644
Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/love.png and b/mobile/android/app/src/main/res/drawable-xhdpi/love.png differ
diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/love.png b/mobile/android/app/src/main/res/drawable-xxhdpi/love.png
index 0e091788..edea8141 100644
Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/love.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/love.png differ
diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/love.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/love.png
index 01a4634f..8dd16c82 100644
Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/love.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/love.png differ
diff --git a/mobile/android/love/src/jni/love/src/common/android.cpp b/mobile/android/love/src/jni/love/src/common/android.cpp
index d7b9230f..2204e45d 100644
--- a/mobile/android/love/src/jni/love/src/common/android.cpp
+++ b/mobile/android/love/src/jni/love/src/common/android.cpp
@@ -37,6 +37,13 @@
#include "filesystem/physfs/PhysfsIo.h"
+// #604 / #839: the SAF bridges below must hand GameActivity the exact
+// directory physfs mounted as the save dir -- the same contract the iOS
+// GRPickerBridge already gets (mobile/ios/patch_love_src.py,
+// gr_saveDirectory) -- instead of letting Java recompute the root on its
+// own, which can name a different volume on merged / adopted-SD storage.
+#include "filesystem/Filesystem.h"
+
namespace love
{
namespace android
@@ -183,6 +190,19 @@ void vibrate(double seconds)
env->DeleteLocalRef(activity);
}
+// The directory physfs actually mounted as the save dir, or "" before the
+// filesystem module is up. GameActivity must copy SAF picks HERE: its own
+// getExternalFilesDir(null) recomputation can disagree with the mounted
+// root on merged / adopted-SD storage (#604, #839).
+static const char *bridgeSaveDirectory()
+{
+ auto fs = Module::getInstance(Module::M_FILESYSTEM);
+ if (fs == nullptr)
+ return "";
+ const char *dir = fs->getSaveDirectory();
+ return dir != nullptr ? dir : "";
+}
+
bool showFilePicker(const char *destFilename)
{
if (destFilename == nullptr || destFilename[0] == '\0')
@@ -192,9 +212,11 @@ bool showFilePicker(const char *destFilename)
jclass activity = env->FindClass("org/love2d/android/GameActivity");
jmethodID method = env->GetStaticMethodID(activity, "showFilePicker",
- "(Ljava/lang/String;)Z");
+ "(Ljava/lang/String;Ljava/lang/String;)Z");
jstring jname = env->NewStringUTF(destFilename);
- jboolean result = env->CallStaticBooleanMethod(activity, method, jname);
+ jstring jsavedir = env->NewStringUTF(bridgeSaveDirectory());
+ jboolean result = env->CallStaticBooleanMethod(activity, method, jname, jsavedir);
+ env->DeleteLocalRef(jsavedir);
env->DeleteLocalRef(jname);
env->DeleteLocalRef(activity);
@@ -210,9 +232,11 @@ bool showCreateDocument(const char *suggestedName)
jclass activity = env->FindClass("org/love2d/android/GameActivity");
jmethodID method = env->GetStaticMethodID(activity, "showCreateDocument",
- "(Ljava/lang/String;)Z");
+ "(Ljava/lang/String;Ljava/lang/String;)Z");
jstring jname = env->NewStringUTF(suggestedName);
- jboolean result = env->CallStaticBooleanMethod(activity, method, jname);
+ jstring jsavedir = env->NewStringUTF(bridgeSaveDirectory());
+ jboolean result = env->CallStaticBooleanMethod(activity, method, jname, jsavedir);
+ env->DeleteLocalRef(jsavedir);
env->DeleteLocalRef(jname);
env->DeleteLocalRef(activity);
@@ -259,7 +283,19 @@ bool httpDownload(const char *url, const char *destPath, const char *userAgent,
return false;
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
- jclass activity = env->FindClass("org/love2d/android/GameActivity");
+ // NOT FindClass: this is the one bridge called off the main thread
+ // (love.thread workers in src/net/fetch_worker.lua and
+ // src/update/check_worker.lua). A worker is a raw pthread whose JNI
+ // class loader is the system one, which cannot see app classes, so
+ // FindClass("org/love2d/android/GameActivity") left a pending
+ // ClassNotFoundException and the next JNI call aborted the process --
+ // opening FIND MODS killed the app on the first stats fetch. Resolving
+ // through the live activity instance works from any attached thread.
+ jobject activityObj = (jobject) SDL_AndroidGetActivity();
+ if (activityObj == nullptr)
+ return false;
+ jclass activity = env->GetObjectClass(activityObj);
+ env->DeleteLocalRef(activityObj);
// Old APK / new liblove skew: report "no transport" the same way a
// missing curl does, instead of aborting on a missing method (#597).
@@ -967,4 +1003,33 @@ void love_android_secondary_enable(int on)
env->DeleteLocalRef(activity);
}
+extern "C" __attribute__((visibility("default")))
+const char *love_android_poll_secondary_touch()
+{
+ static thread_local std::string event;
+ event.clear();
+ JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
+ jclass activity = env->FindClass("org/love2d/android/GameActivity");
+ jmethodID method = env->GetStaticMethodID(activity, "pollSecondaryDisplayTouch",
+ "()Ljava/lang/String;");
+ if (!method)
+ env->ExceptionClear();
+ else
+ {
+ jstring value = (jstring) env->CallStaticObjectMethod(activity, method);
+ if (value)
+ {
+ const char *utf = env->GetStringUTFChars(value, nullptr);
+ if (utf)
+ {
+ event = utf;
+ env->ReleaseStringUTFChars(value, utf);
+ }
+ env->DeleteLocalRef(value);
+ }
+ }
+ env->DeleteLocalRef(activity);
+ return event.empty() ? nullptr : event.c_str();
+}
+
#endif // LOVE_ANDROID
diff --git a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java
index 04c8a827..09fa2148 100644
--- a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java
+++ b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java
@@ -116,6 +116,18 @@ public class GameActivity extends SDLActivity {
// bad ROM instead of installing it (#553).
private String pendingPickFilename = PICKED_ROM_FILENAME;
private static final String STATE_PENDING_PICK = "pendingPickFilename";
+ // Absolute save directory physfs actually mounted, as reported by the
+ // native bridge call that opened the picker (love/src/common/android.cpp,
+ // bridgeSaveDirectory). This activity used to recompute
+ // getExternalFilesDir(null)/save/ on its own at result time; on
+ // merged / adopted-SD storage that can name a different volume than the
+ // one LOVE mounted, so the copied pick (and pick_error.flag) landed where
+ // Lua never scans -- the launcher then "did nothing" after a pick (#604)
+ // and the folders a file manager can browse stayed empty while the game
+ // saved fine elsewhere (#839). Empty string means "not told yet": fall
+ // back to the historical computation.
+ private String pendingPickSaveDir = "";
+ private static final String STATE_PENDING_PICK_DIR = "pendingPickSaveDir";
private static final String STATE_PENDING_CREATE = "pendingCreateSuggestedName";
// Suggested download name for the in-flight SAF create (set by showCreateDocument).
private String pendingCreateSuggestedName = "export.sav";
@@ -186,6 +198,8 @@ public class GameActivity extends SDLActivity {
// a recreated activity still lands under the basename it asked for.
String pick = savedInstanceState.getString(STATE_PENDING_PICK);
if (pick != null) pendingPickFilename = pick;
+ String pickDir = savedInstanceState.getString(STATE_PENDING_PICK_DIR);
+ if (pickDir != null) pendingPickSaveDir = pickDir;
String create = savedInstanceState.getString(STATE_PENDING_CREATE);
if (create != null) pendingCreateSuggestedName = create;
}
@@ -467,13 +481,23 @@ public class GameActivity extends SDLActivity {
* @param destFilename basename under the app save identity (e.g.
* picked_rom.gb, picked_mod.zip, picked_save.sav)
*/
+ /** Legacy single-argument entry; resolves the save dir itself. */
@Keep
public static boolean showFilePicker(String destFilename) {
+ return showFilePicker(destFilename, null);
+ }
+
+ @Keep
+ public static boolean showFilePicker(String destFilename, String saveDir) {
GameActivity self = (GameActivity) mSingleton;
if (self == null) return false;
if (destFilename == null || destFilename.length() == 0) {
destFilename = PICKED_ROM_FILENAME;
}
+ // Remember where LOVE's filesystem is really mounted so
+ // onActivityResult copies the pick there, not into a recomputed
+ // (possibly different-volume) root (#604, #839).
+ self.pendingPickSaveDir = (saveDir != null) ? saveDir : "";
// Reject path separators so a hostile JNI caller cannot escape the
// save identity directory.
if (destFilename.indexOf('/') >= 0 || destFilename.indexOf('\\') >= 0) {
@@ -644,8 +668,14 @@ public class GameActivity extends SDLActivity {
* return degrades on the Lua side (RomImporter export) to "Exported
* inside the app folder", which is the correct pre-KitKat behavior.
*/
+ /** Legacy single-argument entry; resolves the save dir itself. */
@Keep
public static boolean showCreateDocument(String suggestedName) {
+ return showCreateDocument(suggestedName, null);
+ }
+
+ @Keep
+ public static boolean showCreateDocument(String suggestedName, String saveDir) {
if (android.os.Build.VERSION.SDK_INT < 19) return false;
// (see showFilePicker for why the import side got a pre-19 path)
GameActivity self = (GameActivity) mSingleton;
@@ -657,9 +687,12 @@ public class GameActivity extends SDLActivity {
Log.d("GameActivity", "refusing unsafe create name: " + suggestedName);
return false;
}
- File source = new File(
- new File(self.getExternalFilesDir(null), "save"),
- ROM_SAVE_IDENTITY + "/" + PENDING_EXPORT_FILENAME);
+ // Route through the mounted save dir (#604, #839): Lua staged
+ // pending_export.sav where physfs writes, which is not necessarily
+ // where a fresh getExternalFilesDir(null) points on merged /
+ // adopted-SD storage.
+ self.pendingPickSaveDir = (saveDir != null) ? saveDir : "";
+ File source = new File(self.saveIdentityDir(), PENDING_EXPORT_FILENAME);
if (!source.isFile()) {
Log.d("GameActivity", "no pending export at " + source);
return false;
@@ -680,7 +713,21 @@ public class GameActivity extends SDLActivity {
}
private File saveIdentityDir() {
- return new File(new File(getExternalFilesDir(null), "save"), ROM_SAVE_IDENTITY);
+ // Prefer the mounted save dir the last bridge call reported: the
+ // recomputation below can name a different volume than the one LOVE
+ // mounted on merged / adopted-SD storage (#604, #839).
+ if (pendingPickSaveDir != null && pendingPickSaveDir.length() > 0) {
+ return new File(pendingPickSaveDir);
+ }
+ File ext = getExternalFilesDir(null);
+ if (ext == null) {
+ // Shared storage unavailable (ejected / mid-adoption): without
+ // this guard File(null, "save") silently built the RELATIVE
+ // path save/, mkdirs() failed against "/", and the
+ // pick was dropped with no message at all (#604).
+ ext = getFilesDir();
+ }
+ return new File(new File(ext, "save"), ROM_SAVE_IDENTITY);
}
/** Drops a small flag file in the save identity for Lua to consume on focus. */
@@ -875,6 +922,7 @@ public class GameActivity extends SDLActivity {
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(STATE_PENDING_PICK, pendingPickFilename);
+ outState.putString(STATE_PENDING_PICK_DIR, pendingPickSaveDir);
outState.putString(STATE_PENDING_CREATE, pendingCreateSuggestedName);
}
@@ -1277,6 +1325,9 @@ public class GameActivity extends SDLActivity {
// in src/jni/love/src/common/android.cpp.
private static volatile SecondaryPresentation secondaryPresentation;
private static volatile boolean secondaryEnabled = false;
+ private static final int MAX_SECONDARY_TOUCHES = 32;
+ private static final java.util.ArrayDeque secondaryTouches =
+ new java.util.ArrayDeque<>();
@Keep
public static void setSecondaryEnabled(final boolean on) {
@@ -1329,6 +1380,7 @@ public class GameActivity extends SDLActivity {
private static void teardownSecondaryDisplay() {
SecondaryPresentation p = secondaryPresentation;
secondaryPresentation = null;
+ synchronized (secondaryTouches) { secondaryTouches.clear(); }
if (p != null) {
try { p.dismiss(); } catch (Throwable t) {}
}
@@ -1347,6 +1399,13 @@ public class GameActivity extends SDLActivity {
}
}
+ @Keep
+ public static String pollSecondaryDisplayTouch() {
+ synchronized (secondaryTouches) {
+ return secondaryTouches.pollFirst();
+ }
+ }
+
private static class SecondaryPresentation extends android.app.Presentation {
private final FrameView frameView;
@@ -1413,6 +1472,7 @@ public class GameActivity extends SDLActivity {
private final android.graphics.Paint paint = new android.graphics.Paint();
private final Object lock = new Object();
private int fw, fh;
+ private int activePointer = -1;
FrameView(Context context) {
super(context);
@@ -1434,6 +1494,52 @@ public class GameActivity extends SDLActivity {
postInvalidate();
}
+ private void enqueueTouch(String event) {
+ synchronized (secondaryTouches) {
+ if (secondaryTouches.size() >= MAX_SECONDARY_TOUCHES) {
+ secondaryTouches.clear();
+ secondaryTouches.addLast("cancel,0,0");
+ } else {
+ secondaryTouches.addLast(event);
+ }
+ }
+ }
+
+ private int logicalX(float x) {
+ return Math.min(fw - 1, Math.max(0,
+ (int) ((x - dst.left) * fw / dst.width())));
+ }
+
+ private int logicalY(float y) {
+ return Math.min(fh - 1, Math.max(0,
+ (int) ((y - dst.top) * fh / dst.height())));
+ }
+
+ @Override
+ public boolean onTouchEvent(android.view.MotionEvent event) {
+ synchronized (lock) {
+ int action = event.getActionMasked();
+ if (action == android.view.MotionEvent.ACTION_DOWN && fw > 0
+ && dst.contains((int) event.getX(), (int) event.getY())) {
+ activePointer = event.getPointerId(0);
+ enqueueTouch("down," + logicalX(event.getX()) + ","
+ + logicalY(event.getY()));
+ } else if (action == android.view.MotionEvent.ACTION_UP
+ && activePointer >= 0) {
+ int index = event.findPointerIndex(activePointer);
+ if (index >= 0 && fw > 0) {
+ enqueueTouch("up," + logicalX(event.getX(index)) + ","
+ + logicalY(event.getY(index)));
+ }
+ activePointer = -1;
+ } else if (action == android.view.MotionEvent.ACTION_CANCEL) {
+ activePointer = -1;
+ enqueueTouch("cancel,0,0");
+ }
+ }
+ return true;
+ }
+
@Override
protected void onDraw(android.graphics.Canvas canvas) {
synchronized (lock) {
diff --git a/mobile/ios/README.md b/mobile/ios/README.md
index e1a9f48a..5ae5439c 100644
--- a/mobile/ios/README.md
+++ b/mobile/ios/README.md
@@ -1,130 +1,113 @@
-# iOS build (LÖVE 12.0)
+# iOS build
-> **Native ROM/mod/save import.** The iOS build ships a Swift
-> document-picker bridge (`native/GRPickerBridge.swift` + `GRBootstrap.m`)
-> that `patch_love_src.py` wires into the LÖVE tree on every build:
->
-> - `love.system.pickFile("rom"|"mod"|"sav")` and `love.system.createFile`
-> are exposed to Lua on iOS (same contract as love-android's SAF picker:
-> picks land in the save dir as `picked_rom.gb` / `picked_mod.zip` /
-> `picked_save.sav`; exports signal via `export_done.flag`).
-> - The Info.plist overlay enables `UIFileSharingEnabled` +
-> `LSSupportsOpeningDocumentsInPlace`, and `GRBootstrap.m` sweeps
-> `.gb/.gbc/.zip/.sav` files dropped in Documents (Files app / Finder)
-> into the LÖVE save dir on every activation — drop a ROM, open the app,
-> and it imports with no taps.
-> - `src/import/RomImporter.lua` treats iOS as a mobile platform and polls
-> for picker results (iOS pickers are in-process modals, so Android's
-> refocus rescan never fires).
->
-> The note below about a missing "UIDocumentPicker handoff" is
-> resolved by this bridge.
+This directory contains the macOS/Xcode build used to package Gen1 Recomp as
+an iOS app with LÖVE 12.0.
-macOS + Xcode only. Fetches the **LÖVE 12.0** source tree and matching Apple
-dependencies from the official [LÖVE source](https://github.com/love2d/love)
-and [Apple dependencies](https://github.com/love2d/love-apple-dependencies)
-repositories. `conf.lua` declares LÖVE 12.0 on iOS and 11.5 elsewhere.
+## User data location
-Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `12.0`.
+The app uses the public iOS Documents directory as its LÖVE save directory.
+There is no `pokemon-love2d` subdirectory and the app does not create a
+README file there. When browsing `On My iPhone > gen1recomp` in Files, the
+directory contains the app's runtime data directly, including:
-## Quick start (simulator)
+- installed mods and downloaded ROMs
+- save files and save-state data
+- options, caches, logs, and other files created by the game
+
+The build enables `UIFileSharingEnabled` and
+`LSSupportsOpeningDocumentsInPlace`, so the same directory is available in
+Files and Finder. Files copied into the app's Documents directory are used by
+the game on its next activation.
+
+Existing installations are migrated automatically. Files from the old
+private `Application Support/pokemon-love2d` directory are merged into
+Documents on launch; conflicts are retained with a `.legacy` suffix.
+
+## Build
+
+Run these commands from the repository root:
```bash
-# Fetch LÖVE 12.0 iOS sources and dependencies (once) + build for Simulator
scripts/build_ios.sh --fetch
+scripts/build_ios.sh
```
-The embedded `game.love` contains no ROM or generated game data. The current
-first-boot importer has desktop file pickers only, so a production iOS release
-still needs a UIDocumentPicker handoff that passes the selected ROM to LÖVE.
+`--fetch` downloads the pinned LÖVE source and matching Apple dependencies
+into the gitignored `love-src/` directory. It is only needed when that tree is
+missing. The default build targets the iOS Simulator in Debug configuration.
-Default output: an unsigned Simulator `.app` under `mobile/ios/build/`
-(no Apple Developer account required). A convenience copy also lands under
-`dist/ios/-/`.
-
-Install on a booted simulator (example):
+For a physical device or a release build:
```bash
-xcrun simctl install booted mobile/ios/build/Build/Products/Debug-iphonesimulator/PokemonRed.app
-xcrun simctl launch booted com.theboisclub.pokemonred
+scripts/build_ios.sh --device --install
+scripts/build_ios.sh --device --release --install
```
-Or open `mobile/ios/love-src/platform/xcode/love.xcodeproj` in Xcode,
-select the `love-ios` target, and Run on a Simulator after
-`scripts/build_ios.sh --package-only` (or a full build) has placed `game.love`.
+Device builds require a paired, unlocked device and a valid Apple signing
+identity. Set `DEVELOPMENT_TEAM` or `CODE_SIGN_IDENTITY` when automatic
+signing cannot select the intended account. Add `--ipa` to create
+`dist/ios/gen1recomp.ipa`.
-## Device / Release
+The script verifies the final app before packaging it:
-```bash
-scripts/build_ios.sh --device # Debug, physical device SDK
-scripts/build_ios.sh --device --release # Release configuration
+- the public Documents plist settings are present
+- the native picker bridge is present
+- `game.love` exists and is non-empty
+
+If the payload is missing, the build fails instead of producing a blank app.
+
+## Useful options
+
+| Option | Purpose |
+| --- | --- |
+| `--fetch` | Fetch LÖVE 12.0 and Apple dependencies when `love-src/` is missing |
+| `--device` | Build for `iphoneos` instead of the Simulator |
+| `--release` | Use the Release configuration |
+| `--install` | Install a device build on the first connected device |
+| `--ipa` | Create an IPA after a device build |
+| `--version X.Y.Z` | Stamp the engine and app version |
+| `--package-only` | Package `game.love` and apply the iOS plist overlay without Xcode |
+
+`scripts/build.sh ios` delegates to this script and forwards the iOS release
+option.
+
+## Output
+
+Simulator and device app bundles are copied to:
+
+```text
+dist/ios/Debug-iphonesimulator/gen1recomp.app
+dist/ios/Release-iphonesimulator/gen1recomp.app
+dist/ios/Debug-iphoneos/gen1recomp.app
+dist/ios/Release-iphoneos/gen1recomp.app
```
-Device builds need a signing identity and provisioning profile configured in
-Xcode (or via `DEVELOPMENT_TEAM` / `CODE_SIGN_IDENTITY` env vars). This repo
-does **not** store certificates, profiles, or App Store Connect secrets.
+The intermediate Xcode products are under `mobile/ios/build/`. Both locations
+are gitignored.
-Manual out-of-band steps:
-
-1. Apple Developer account + App ID for `com.theboisclub.pokemonred`
-2. Development or Distribution certificate + provisioning profile
-3. In Xcode: open `love.xcodeproj` → target `love-ios` → Signing & Capabilities
- → select your Team (or set `DEVELOPMENT_TEAM=XXXXXXXXXX` when invoking
- `scripts/build_ios.sh --device`)
-4. Archive / export an `.ipa` from Xcode Organizer for TestFlight / Ad Hoc
-
-## Layout
-
-| Path | Role |
-|------|------|
-| `LOVE_VERSION` | Engine pin (`12.0`) |
-| `overlays/love-ios.plist` | Portrait-only Info.plist + display name **Pokemon Red** (copied over the upstream plist every build) |
-| `love-src/` | Downloaded LÖVE 12.0 source tree (**gitignored**, do not commit) |
-| `cache/` | Temporary source and dependency checkout data (**gitignored**) |
-| `build/` | `xcodebuild` derived data (**gitignored**) |
-
-Game payload lands at:
-
-`love-src/platform/xcode/ios/resources/game.love`
-
-and is fused into the built `.app` (LÖVE auto-runs any bundled `*.love`).
-
-## Apple libraries dependency
-
-`scripts/build_ios.sh --fetch` retrieves the matching iOS libraries and the
-SDL3 framework from
-[love-apple-dependencies](https://github.com/love2d/love-apple-dependencies).
-Re-run it if either dependency directory is absent.
+The bundled game payload is staged at
+`love-src/platform/xcode/ios/resources/game.love` and copied into the final
+app bundle. The payload contains the game, not user-generated ROMs, mods, or
+saves; those are created at runtime in Documents.
## App identity
-| Field | Value |
-|-------|--------|
-| Display name | Pokemon Red |
-| `PRODUCT_NAME` | PokemonRed |
-| Bundle ID | `com.theboisclub.pokemonred` |
-| Orientations | Portrait only (`UIInterfaceOrientationPortrait`) |
+| Field | Default |
+| --- | --- |
+| Display name | `gen1recomp` |
+| Product name | `gen1recomp` |
+| Bundle identifier | `com.theboisclub.gen1recomp` |
+| Save directory | Public `Documents` root |
+| Orientation | Portrait |
-Overrides are applied by the build script (`xcodebuild` settings + plist overlay)
-so refreshing `love-src/` does not lose branding.
+Set `GEN1_BUNDLE_ID` to use a different bundle identifier for local device
+builds.
-## Flags (`scripts/build_ios.sh`)
+## Prerequisites
-| Flag | Meaning |
-|------|---------|
-| *(default)* | Simulator, Debug, no signing |
-| `--fetch` | Fetch the LÖVE 12.0 source tree and Apple dependencies if `love-src/` is missing |
-| `--device` | Build against `iphoneos` instead of `iphonesimulator` |
-| `--release` | `Release` configuration instead of `Debug` |
-| `--package-only` | Zip `game.love` + apply plist overlay; skip `xcodebuild` |
+- macOS with Xcode and `xcodebuild`
+- the iOS and iOS Simulator platforms installed in Xcode
+- a fetched `love-src/` tree, or the `--fetch` option
+- the matching iOS libraries and SDL3 framework under `love-src/`
-Also: `scripts/build.sh ios` delegates here (`--release` is forwarded).
-
-## Preconditions
-
-- macOS (Darwin) with Xcode + `xcodebuild` on `PATH`
-- iOS platform installed in Xcode (Settings → Platforms). `xcodebuild -showsdks`
- should list `iphonesimulator` / `iphoneos`. A partial install can fail IB/xib
- compiles with `iOS … Platform Not Installed` even when the SDK name appears.
-- `love-src/` present (`--fetch`)
-- iOS libraries under `love-src/platform/xcode/ios/libraries/` and SDL3 under `love-src/platform/xcode/shared/Frameworks/`
+Use `xcodebuild -showsdks` to confirm that the required SDKs are installed.
diff --git a/mobile/ios/native/GRBootstrap.m b/mobile/ios/native/GRBootstrap.m
index 4b56de9c..de7a59e8 100644
--- a/mobile/ios/native/GRBootstrap.m
+++ b/mobile/ios/native/GRBootstrap.m
@@ -16,6 +16,9 @@ static void GRBootstrapInstall(void)
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
Class bridge = NSClassFromString(@"GRPickerBridge");
+ if ([bridge respondsToSelector:@selector(preparePublicDocuments)]) {
+ [bridge performSelector:@selector(preparePublicDocuments)];
+ }
if ([bridge respondsToSelector:@selector(sweepInbox)]) {
[bridge performSelector:@selector(sweepInbox)];
}
diff --git a/mobile/ios/native/GRPickerBridge.swift b/mobile/ios/native/GRPickerBridge.swift
index 9f3e3966..c571461b 100644
--- a/mobile/ios/native/GRPickerBridge.swift
+++ b/mobile/ios/native/GRPickerBridge.swift
@@ -26,8 +26,6 @@ public final class GRPickerBridge: NSObject {
// silently doing nothing.
private static var liveDelegates: [PickerDelegate] = []
- // conf.lua t.identity — where LÖVE puts the fused save directory on iOS
- // (/Library/Application Support/).
private static let loveIdentity = "pokemon-love2d"
@objc(httpDownloadWithUrl:destination:userAgent:accept:)
@@ -85,11 +83,30 @@ public final class GRPickerBridge: NSObject {
types = [.zip]
case "sav":
destName = "picked_save.sav"
- default:
+ // A Nintendo 64 cartridge, for mods that build assets out of one --
+ // the voxel mod's Pokemon Stadium battle models are the caller this
+ // was added for. Its own filename on purpose: an N64 ROM landing on
+ // picked_rom.gb is swept up by the Game Boy importer, deleted, and
+ // reported to the player as a broken cartridge.
+ case "stadium":
+ destName = "picked_stadium.z64"
+ for ext in ["z64", "n64", "v64"] {
+ if let t = UTType(filenameExtension: ext) { types.append(t) }
+ }
+ case "rom", "":
destName = "picked_rom.gb"
for ext in ["gb", "gbc"] {
if let t = UTType(filenameExtension: ext) { types.append(t) }
}
+ // An unknown kind is REFUSED rather than treated as a Game Boy ROM.
+ //
+ // It used to fall through to picked_rom.gb, so a caller asking for a
+ // kind this build had never heard of got its file deleted and
+ // reported as a broken cartridge -- the worst possible answer to
+ // "I do not know that one". Returning false lets the caller find out
+ // and offer its own fallback.
+ default:
+ return false
}
// .gb/.gbc/.sav resolve to dynamic UTTypes on most devices; offering
// .data as well keeps every real file selectable. The importer
@@ -107,6 +124,20 @@ public final class GRPickerBridge: NSObject {
return present(picker, with: delegate)
}
+ // Which kinds presentPicker understands, comma separated.
+ //
+ // So a CALLER can ask before it calls. A mod that wants a kind this build
+ // predates cannot otherwise tell "refused" from "the picker would not
+ // open", and guessing wrong used to cost the player their ROM (see the
+ // default case above). Asking first turns that into a fallback the caller
+ // chooses rather than a file it loses.
+ //
+ // Kept beside the switch it describes, because the two drifting apart is
+ // the only way this can lie.
+ @objc public static func supportedPickerKinds() -> NSString {
+ return "rom,mod,sav,stadium" as NSString
+ }
+
@objc(presentExportWithName:saveDir:)
public static func presentExport(name: UnsafePointer?,
saveDir: UnsafePointer?) -> Bool {
@@ -144,10 +175,10 @@ public final class GRPickerBridge: NSObject {
// UIApplicationDidBecomeActive (see GRBootstrap.m).
@objc public static func sweepInbox() {
let fm = FileManager.default
- guard let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first,
- let appSupport = fm.urls(for: .applicationSupportDirectory,
- in: .userDomainMask).first else { return }
- let saveDir = appSupport.appendingPathComponent(loveIdentity, isDirectory: true)
+ migrateLegacySaveDirectory()
+ guard let docs = documentsDirectory(),
+ let saveDir = publicSaveDirectory() else { return }
+ guard docs.standardizedFileURL != saveDir.standardizedFileURL else { return }
let wanted: Set = ["gb", "gbc", "zip", "sav"]
guard let items = try? fm.contentsOfDirectory(at: docs,
includingPropertiesForKeys: nil) else { return }
@@ -164,15 +195,21 @@ public final class GRPickerBridge: NSObject {
}
}
+ @objc public static func preparePublicDocuments() {
+ migrateLegacySaveDirectory()
+ if let saveDir = publicSaveDirectory() {
+ ensureDirectory(saveDir)
+ }
+ }
+
// MARK: - Helpers
private static func resolvedSaveDir(_ cstr: UnsafePointer?) -> URL? {
var dir = cstr.map { String(cString: $0) } ?? ""
if dir.isEmpty {
- guard let appSupport = FileManager.default
- .urls(for: .applicationSupportDirectory, in: .userDomainMask).first
- else { return nil }
- dir = appSupport.appendingPathComponent(loveIdentity).path
+ migrateLegacySaveDirectory()
+ guard let saveDir = publicSaveDirectory() else { return nil }
+ dir = saveDir.path
}
let url = URL(fileURLWithPath: dir, isDirectory: true)
ensureDirectory(url)
@@ -184,6 +221,75 @@ public final class GRPickerBridge: NSObject {
withIntermediateDirectories: true)
}
+ private static func documentsDirectory() -> URL? {
+ FileManager.default.urls(for: .documentDirectory,
+ in: .userDomainMask).first
+ }
+
+ private static func publicSaveDirectory() -> URL? {
+ documentsDirectory()
+ }
+
+ private static func legacySaveDirectory() -> URL? {
+ FileManager.default.urls(for: .applicationSupportDirectory,
+ in: .userDomainMask).first?
+ .appendingPathComponent(loveIdentity, isDirectory: true)
+ }
+
+ private static func migrateLegacySaveDirectory() {
+ let fm = FileManager.default
+ guard let destination = publicSaveDirectory(),
+ let legacy = legacySaveDirectory(),
+ fm.fileExists(atPath: legacy.path) else {
+ return
+ }
+ ensureDirectory(destination)
+ mergeDirectory(from: legacy, to: destination)
+ try? fm.removeItem(at: legacy)
+ }
+
+ private static func mergeDirectory(from source: URL, to destination: URL) {
+ let fm = FileManager.default
+ ensureDirectory(destination)
+ guard let items = try? fm.contentsOfDirectory(at: source,
+ includingPropertiesForKeys: nil)
+ else { return }
+ for item in items {
+ let target = destination.appendingPathComponent(item.lastPathComponent)
+ var sourceIsDirectory = ObjCBool(false)
+ fm.fileExists(atPath: item.path, isDirectory: &sourceIsDirectory)
+ var targetIsDirectory = ObjCBool(false)
+ let targetExists = fm.fileExists(atPath: target.path,
+ isDirectory: &targetIsDirectory)
+ if sourceIsDirectory.boolValue && targetExists && targetIsDirectory.boolValue {
+ mergeDirectory(from: item, to: target)
+ continue
+ }
+ if targetExists {
+ if !sourceIsDirectory.boolValue && !targetIsDirectory.boolValue &&
+ fm.contentsEqual(atPath: item.path, andPath: target.path) {
+ try? fm.removeItem(at: item)
+ } else {
+ moveToLegacyName(item, in: destination)
+ }
+ continue
+ }
+ try? fm.moveItem(at: item, to: target)
+ }
+ }
+
+ private static func moveToLegacyName(_ item: URL, in destination: URL) {
+ let fm = FileManager.default
+ let base = item.lastPathComponent + ".legacy"
+ var target = destination.appendingPathComponent(base)
+ var suffix = 2
+ while fm.fileExists(atPath: target.path) {
+ target = destination.appendingPathComponent("\(base).\(suffix)")
+ suffix += 1
+ }
+ try? fm.moveItem(at: item, to: target)
+ }
+
private static func copyItem(at src: URL, into dir: URL, named name: String) {
let scoped = src.startAccessingSecurityScopedResource()
defer { if scoped { src.stopAccessingSecurityScopedResource() } }
diff --git a/mobile/ios/patch_love_src.py b/mobile/ios/patch_love_src.py
index ef9d1b39..b6d6eb40 100644
--- a/mobile/ios/patch_love_src.py
+++ b/mobile/ios/patch_love_src.py
@@ -27,6 +27,8 @@ NATIVE_SRC = IOS_DIR / "native"
NATIVE_DST = LOVE_SRC / "platform" / "xcode" / "ios" / "native"
WRAP_SYSTEM = LOVE_SRC / "src" / "modules" / "system" / "wrap_System.cpp"
PBXPROJ = LOVE_SRC / "platform" / "xcode" / "love.xcodeproj" / "project.pbxproj"
+APPLE_MM = LOVE_SRC / "src" / "common" / "apple.mm"
+FILESYSTEM_CPP = LOVE_SRC / "src" / "modules" / "filesystem" / "physfs" / "Filesystem.cpp"
ENTITLEMENTS_SRC = IOS_DIR / "overlays" / "love-ios.entitlements"
NATIVE_FILES = ("GRPickerBridge.swift", "GRHealthBridge.swift", "GRBootstrap.m")
@@ -84,6 +86,49 @@ int w_pickFile(lua_State *L)
return gr_callBridge(L, "GRPickerBridge", "presentPickerWithKind:saveDir:", kind);
}
+// love.system.pickFileKinds() -> "rom,mod,sav,stadium", or nil off iOS.
+//
+// So a caller can ask what this build's picker understands BEFORE opening it.
+// An unknown kind is refused (GRPickerBridge), and a refusal looks exactly
+// like a picker that would not open -- so a caller with a fallback worth
+// showing needs to know which it is facing. A mod that guesses instead has
+// no way back: before the refusal landed, an unrecognised kind wrote
+// picked_rom.gb and the ROM importer deleted it.
+//
+// nil where there is no bridge at all, which reads the same as "no kinds".
+int w_pickFileKinds(lua_State *L)
+{
+ Class cls = objc_getClass("GRPickerBridge");
+ if (cls == nullptr)
+ {
+ lua_pushnil(L);
+ return 1;
+ }
+ // Fetched through the runtime: wrap_System.cpp is compiled as C++ rather
+ // than Objective-C++, so no Foundation type may be NAMED here -- writing
+ // `NSString` alone breaks the whole translation unit. objc_msgSend is a
+ // plain C entry point and `id` comes from objc/runtime.h, so the string
+ // is asked for its UTF8 bytes without ever being typed.
+ typedef id (*GRObj)(Class, SEL);
+ id kinds = ((GRObj)objc_msgSend)(cls,
+ sel_registerName("supportedPickerKinds"));
+ if (kinds == nullptr)
+ {
+ lua_pushnil(L);
+ return 1;
+ }
+ typedef const char *(*GRUTF8)(id, SEL);
+ const char *bytes = ((GRUTF8)objc_msgSend)(kinds,
+ sel_registerName("UTF8String"));
+ if (bytes == nullptr || bytes[0] == '\0')
+ {
+ lua_pushnil(L);
+ return 1;
+ }
+ lua_pushstring(L, bytes);
+ return 1;
+}
+
int w_createFile(lua_State *L)
{
const char *name = luaL_optstring(L, 1, "export.sav");
@@ -101,6 +146,7 @@ int w_syncHealthSteps(lua_State *L)
WRAP_REGISTRATION = """#ifdef LOVE_IOS
{ "pickFile", w_pickFile },
+ { "pickFileKinds", w_pickFileKinds },
{ "createFile", w_createFile },
{ "syncHealthSteps", w_syncHealthSteps },
{ "httpDownload", w_httpDownload },
@@ -201,19 +247,20 @@ def fail(msg):
sys.exit(1)
-def pristine(path: Path) -> str:
+def pristine(path: Path, patched_markers=None) -> str:
"""Text of `path` before any of our patching: backed by a `.orig` stash.
The stash is only trusted if it is itself unpatched; that protects
against a stash accidentally taken after an earlier patch run.
"""
+ patched_markers = tuple(patched_markers or (MARKER, ID_FILE_PICKER))
orig = path.with_suffix(path.suffix + ".orig")
if orig.is_file():
text = orig.read_text()
- if MARKER not in text and ID_FILE_PICKER not in text:
+ if not any(marker in text for marker in patched_markers):
return text
text = path.read_text()
- if MARKER in text or ID_FILE_PICKER in text:
+ if any(marker in text for marker in patched_markers):
fail(f"{path} is already patched and no pristine .orig stash exists;\n"
f" delete {LOVE_SRC} and re-run scripts/build_ios.sh --fetch")
orig.write_text(text)
@@ -255,6 +302,62 @@ def patch_wrap_system():
"(pickFile/createFile/syncHealthSteps/httpDownload)")
+def patch_public_documents():
+ text = pristine(
+ APPLE_MM,
+ ("#ifdef LOVE_IOS\n"
+ "\t\t\tnsdir = NSDocumentDirectory;\n"
+ "#else\n",),
+ )
+ original = (
+ "\t\tcase USER_DIRECTORY_APPSUPPORT:\n"
+ "\t\t\tnsdir = NSApplicationSupportDirectory;\n"
+ "\t\t\tbreak;"
+ )
+ replacement = (
+ "\t\tcase USER_DIRECTORY_APPSUPPORT:\n"
+ "#ifdef LOVE_IOS\n"
+ "\t\t\tnsdir = NSDocumentDirectory;\n"
+ "#else\n"
+ "\t\t\tnsdir = NSApplicationSupportDirectory;\n"
+ "#endif\n"
+ "\t\t\tbreak;"
+ )
+ if original not in text:
+ fail(f"iOS app-support path anchor not found in {APPLE_MM}")
+ APPLE_MM.write_text(text.replace(original, replacement, 1))
+
+ filesystem_text = pristine(
+ FILESYSTEM_CPP,
+ ("#ifdef LOVE_IOS\n"
+ "\t\t\tsuffix.clear();\n"
+ "#else\n",),
+ )
+ filesystem_original = (
+ "\t\tstd::string suffix;\n"
+ "\t\tif (isFused())\n"
+ "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR) + saveIdentity;\n"
+ "\t\telse\n"
+ "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + saveIdentity;"
+ )
+ filesystem_replacement = (
+ "\t\tstd::string suffix;\n"
+ "#ifdef LOVE_IOS\n"
+ "\t\t\tsuffix.clear();\n"
+ "#else\n"
+ "\t\tif (isFused())\n"
+ "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR) + saveIdentity;\n"
+ "\t\telse\n"
+ "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + saveIdentity;\n"
+ "#endif"
+ )
+ if filesystem_original not in filesystem_text:
+ fail(f"iOS save directory suffix anchor not found in {FILESYSTEM_CPP}")
+ FILESYSTEM_CPP.write_text(filesystem_text.replace(filesystem_original,
+ filesystem_replacement, 1))
+ print("patch_love_src: iOS save directory routed to Documents root")
+
+
def patch_pbxproj():
text = pristine(PBXPROJ)
@@ -318,6 +421,7 @@ def main():
if not LOVE_SRC.is_dir():
fail("love-src/ missing; run scripts/build_ios.sh --fetch first")
copy_native_files()
+ patch_public_documents()
patch_wrap_system()
patch_pbxproj()
diff --git a/mods/spanish_ui/README.md b/mods/spanish_ui/README.md
new file mode 100644
index 00000000..b844ac80
--- /dev/null
+++ b/mods/spanish_ui/README.md
@@ -0,0 +1,27 @@
+# spanish_ui
+
+A Espanol translation of the game.
+
+Generated with `python3 tools/modkit.py translation spanish_ui`. See
+`TRANSLATING.md` for how to work on it.
+
+## Status
+
+Nothing is translated yet: 601 strings are waiting in `lang/`.
+
+| Catalog | Entries |
+|---|---|
+| `lang/dialogue.lua` | 6 |
+| `lang/strings.lua` | 577 |
+| `lang/species_names.lua` | 3 |
+| `lang/move_names.lua` | 4 |
+| `lang/item_names.lua` | 5 |
+| `lang/trainer_names.lua` | 1 |
+| `lang/status_labels.lua` | 5 |
+
+## Layout
+
+- `manifest.json` - identity and the engine version range
+- `main.lua` - registers whatever is filled in and skips whatever is not
+- `lang/` - the catalogs; this is the whole job
+- `assets/font/` - your glyph sheet
diff --git a/mods/spanish_ui/TRANSLATING.md b/mods/spanish_ui/TRANSLATING.md
new file mode 100644
index 00000000..f1cca9bc
--- /dev/null
+++ b/mods/spanish_ui/TRANSLATING.md
@@ -0,0 +1,111 @@
+# Translating into Espanol
+
+Everything the player can read is one of two kinds of string, and they live
+in different places for a reason.
+
+| lang/ file | What it is | Key |
+|---|---|---|
+| `dialogue.lua` | Every line of extracted script text | the original label, e.g. `_PalletTownText1` |
+| `strings.lua` | Text the engine itself writes: battle messages, menus, link play | the English source string |
+| `species.lua` `moves.lua` `items.lua` `trainers.lua` | Names | the vanilla id |
+| `statuses.lua` | `PSN`, `BRN`, ... as they appear in the HUD | the status id |
+| `font.lua` `charmap.lua` | Your glyph sheet and what draws what | see below |
+| `naming.lua` | The letter grid for entering names | - |
+
+Fill in a value and it takes effect. Leave it `""` and that string stays in
+English, so the game is playable at every point along the way.
+
+## Where the English is
+
+The catalogs hold keys and *your* text, never the original English. The
+English lives next door, in `spanish_ui-worksheet/`, one tab-separated file per
+catalog:
+
+```
+"_AbandonLearningText" "Abandon learning\n{RAM:wStringBuffer}?"
+```
+
+That directory is deliberately outside the mod. Extracted script text and
+the vanilla names are ROM content, and `modkit pack` zips everything under
+the mod directory, so a worksheet kept inside would end up in your release
+whatever a `.gitignore` said. Keep it beside the mod, never in it.
+
+`lang/strings.lua` is the exception: those sources are the engine's own Lua
+rather than anything out of the ROM, so there the key *is* the English and
+you can translate straight from it.
+
+## Start with the font, not the text
+
+The engine draws from **glyph pages**: an image of 8x8 cells plus a charmap
+saying which byte sequence draws which cell. The vanilla pages sit at `$60`
+and `$80`. Anything from `0x100` up is free, so a new alphabet is added
+rather than swapped in:
+
+```lua
+-- lang/font.lua
+return {
+ spanish_ui = {
+ image = "assets/font/spanish_ui.png",
+ base = 0x100, -- first code this page owns
+ glyphsPerRow = 16,
+ -- advance = 8, -- set this if your glyphs are not 8px wide
+ },
+}
+```
+
+```lua
+-- lang/charmap.lua: sequence -> code, in the same order as the sheet
+return {
+ ["A"] = 0x100,
+ ["B"] = 0x101,
+}
+```
+
+The sheet is a plain PNG, 16 glyphs to a row by default, each cell 8x8,
+black on white like `assets/generated/font.png`. Codes run left to right,
+top to bottom from `base`.
+
+Sequences are matched **longest first**, so a multi-byte character and a
+multi-character ligature both work and neither shadows the other:
+
+```lua
+["\u{3042}"] = 0x120, -- one 3-byte character, one glyph
+["ch"] = 0x121, -- two ASCII letters, one glyph
+```
+
+## Line length is counted in glyphs
+
+The dialogue box fits 18 glyphs a line, not 18 bytes. A 3-byte character
+costs one column, and the engine will never cut a character in half. Your
+own `\n` line breaks are respected exactly as written, so break lines where
+they read best rather than where they fit English.
+
+If your glyphs are not 8px wide, set `advance` on the page and the box
+re-measures.
+
+## Format directives must survive
+
+Some sources carry `%s` or `%d`:
+
+```lua
+["Wild %s\nappeared!"] = "...",
+```
+
+Keep every directive, in a count that matches. Word order is yours to
+change; the engine substitutes in the order the directives appear, so if
+your language needs the name last, write the sentence with the `%s` last.
+A translation whose directive count does not match the English is refused
+at runtime and the English is drawn instead, with a line in the log saying
+so - it will not crash a battle.
+
+## Checking your work
+
+```sh
+python3 tools/modkit.py validate spanish_ui --base imported
+python3 tools/modkit.py translation spanish_ui --refresh # pick up new engine strings
+POKEPORT_DEV=1 scripts/run.sh # F5 hot-reloads lang/
+```
+
+`--refresh` rewrites the catalogs from the current engine, keeping every
+translation you have already written and reporting what changed. Run it
+after pulling a new engine version.
diff --git a/mods/spanish_ui/assets/font/README.md b/mods/spanish_ui/assets/font/README.md
new file mode 100644
index 00000000..8f0f5890
--- /dev/null
+++ b/mods/spanish_ui/assets/font/README.md
@@ -0,0 +1,11 @@
+Put your glyph sheet here.
+
+A page is a PNG of 8x8 cells, 16 per row by default, black on white. Codes
+run left to right and top to bottom starting at the page's `base`, so the
+first cell is `base`, the second `base + 1`, and so on.
+
+`assets/generated/font.png` in the player's cache is the vanilla sheet at
+the same scale; open it alongside yours to match weight and baseline.
+
+Declare the sheet in `lang/font.lua` and map sequences to codes in
+`lang/charmap.lua`.
diff --git a/mods/spanish_ui/lang/charmap.lua b/mods/spanish_ui/lang/charmap.lua
new file mode 100644
index 00000000..ca0e3140
--- /dev/null
+++ b/mods/spanish_ui/lang/charmap.lua
@@ -0,0 +1,10 @@
+-- Which byte sequence draws which glyph code.
+--
+-- Sequences are matched longest-first, so a multi-byte character and a
+-- multi-character ligature both work: "ch" can be one glyph even though
+-- "c" is also mapped. Codes here must land inside a page declared in
+-- lang/font.lua.
+return {
+ -- ["A"] = 0x100,
+ -- ["B"] = 0x101,
+}
diff --git a/mods/spanish_ui/lang/dialogue.lua b/mods/spanish_ui/lang/dialogue.lua
new file mode 100644
index 00000000..8d3c16f3
--- /dev/null
+++ b/mods/spanish_ui/lang/dialogue.lua
@@ -0,0 +1,12 @@
+-- Script text
+--
+-- Keyed by the original text label. The English is in the comment.
+
+return {
+ ["_FixMartText"] = "",
+ ["_FixRouteTrainerAfterText"] = "",
+ ["_FixRouteTrainerBattleText"] = "",
+ ["_FixRouteTrainerEndText"] = "",
+ ["_FixTownGreeterText"] = "",
+ ["_FixTownSignText"] = "",
+}
diff --git a/mods/spanish_ui/lang/font.lua b/mods/spanish_ui/lang/font.lua
new file mode 100644
index 00000000..4b878079
--- /dev/null
+++ b/mods/spanish_ui/lang/font.lua
@@ -0,0 +1,13 @@
+-- Glyph pages this translation adds. Delete the entry if the vanilla
+-- alphabet already covers your language.
+--
+-- base is the first glyph code the page owns. 0x100 and up is free space
+-- above the vanilla $60/$80 pages, so this adds an alphabet rather than
+-- replacing one. Set `advance` if your glyphs are not 8px wide.
+return {
+ -- spanish_ui = {
+ -- image = "assets/font/spanish_ui.png",
+ -- base = 0x100,
+ -- glyphsPerRow = 16,
+ -- },
+}
diff --git a/mods/spanish_ui/lang/item_names.lua b/mods/spanish_ui/lang/item_names.lua
new file mode 100644
index 00000000..2798d181
--- /dev/null
+++ b/mods/spanish_ui/lang/item_names.lua
@@ -0,0 +1,11 @@
+-- Item names
+--
+-- Item names for Espanol.
+
+return {
+ ["FIX_BADGE_1"] = "",
+ ["FIX_BADGE_2"] = "",
+ ["FIX_BALL"] = "",
+ ["FIX_POTION"] = "",
+ ["FIX_TM"] = "",
+}
diff --git a/mods/spanish_ui/lang/move_names.lua b/mods/spanish_ui/lang/move_names.lua
new file mode 100644
index 00000000..80abf686
--- /dev/null
+++ b/mods/spanish_ui/lang/move_names.lua
@@ -0,0 +1,10 @@
+-- Move names
+--
+-- Move names for Espanol.
+
+return {
+ ["FIX_CUT"] = "",
+ ["FIX_EMBERISH"] = "",
+ ["FIX_SCRATCH"] = "",
+ ["FIX_TACKLE"] = "",
+}
diff --git a/mods/spanish_ui/lang/naming.lua b/mods/spanish_ui/lang/naming.lua
new file mode 100644
index 00000000..adb0666b
--- /dev/null
+++ b/mods/spanish_ui/lang/naming.lua
@@ -0,0 +1,41 @@
+-- The naming screen's letter grid. Return an empty table to keep the
+-- English alphabet.
+--
+-- Each entry is a row of cells; a cell is whatever sequence your charmap
+-- maps, so a multi-byte character is one cell. The row holding a single
+-- "lower case" / "UPPER CASE" cell is the case switch, and the cell
+-- spelled "ED" is the confirm.
+--
+-- The screen is 160x144 and NamingScreen draws cell `c` of row `r` at
+-- (c * 16, 32 + r * 16), so the grid is capped at **9 columns and 6 rows**:
+-- a 10th column lands at x=160 and a 7th row at y=144, both off screen.
+-- That leaves 44 usable cells, exactly what vanilla uses, so Spanish
+-- letters have to displace something rather than being added.
+--
+-- What gives way is vanilla's `× ( ) : ; [ ]` row. Those are legal in a
+-- Gen-1 nickname but nobody reaches for them, whereas Ñ is not optional in
+-- Spanish -- and here it sits in its alphabetical place after N, which is
+-- where a Spanish speaker will look for it. Space, and are kept.
+--
+-- These glyphs exist in the Spanish cartridge's font ($CA Ñ, $BF Á, $C7 É,
+-- $C9 Í, $CC Ó, $CE Ú, $C2 Ü and their lowercase). On an English ROM they
+-- do not, so main.lua checks the running game's charmap first and keeps the
+-- English grid rather than drawing blank cells.
+return {
+ upper = {
+ { "A", "B", "C", "D", "E", "F", "G", "H", "I" },
+ { "J", "K", "L", "M", "N", "Ñ", "O", "P", "Q" },
+ { "R", "S", "T", "U", "V", "W", "X", "Y", "Z" },
+ { "Á", "É", "Í", "Ó", "Ú", "Ü", " ", "", "" },
+ { "-", "?", "!", "♂", "♀", "/", ".", ",", "ED" },
+ { "lower case" },
+ },
+ lower = {
+ { "a", "b", "c", "d", "e", "f", "g", "h", "i" },
+ { "j", "k", "l", "m", "n", "ñ", "o", "p", "q" },
+ { "r", "s", "t", "u", "v", "w", "x", "y", "z" },
+ { "á", "é", "í", "ó", "ú", "ü", " ", "", "" },
+ { "-", "?", "!", "♂", "♀", "/", ".", ",", "ED" },
+ { "UPPER CASE" },
+ },
+}
diff --git a/mods/spanish_ui/lang/species_names.lua b/mods/spanish_ui/lang/species_names.lua
new file mode 100644
index 00000000..7a4e5645
--- /dev/null
+++ b/mods/spanish_ui/lang/species_names.lua
@@ -0,0 +1,9 @@
+-- Species names
+--
+-- Species names for Espanol.
+
+return {
+ ["FIXMON_A"] = "",
+ ["FIXMON_B"] = "",
+ ["FIXMON_C"] = "",
+}
diff --git a/mods/spanish_ui/lang/status_labels.lua b/mods/spanish_ui/lang/status_labels.lua
new file mode 100644
index 00000000..b3cd3365
--- /dev/null
+++ b/mods/spanish_ui/lang/status_labels.lua
@@ -0,0 +1,11 @@
+-- Status labels
+--
+-- Short enough for the battle HUD: the vanilla ones are three glyphs.
+
+return {
+ ["BRN"] = "",
+ ["FRZ"] = "",
+ ["PAR"] = "",
+ ["PSN"] = "",
+ ["SLP"] = "",
+}
diff --git a/mods/spanish_ui/lang/strings.lua b/mods/spanish_ui/lang/strings.lua
new file mode 100644
index 00000000..c0858d3c
--- /dev/null
+++ b/mods/spanish_ui/lang/strings.lua
@@ -0,0 +1,584 @@
+-- Engine text
+--
+-- Keyed by the English source, which is also what draws if you leave
+-- an entry empty. Keep any %s / %d directives.
+
+return {
+ ["%s\nflew up high!"] = "¡%s\nvoló muy alto!",
+ ["%s\ndug a hole!"] = "¡%s\ncavó un hoyo!",
+ ["%s\nmade a whirlwind!"] = "¡%s\ncreó un torbellino!",
+ ["%s\ntook in sunlight!"] = "¡%s\nabsorbió luz!",
+ ["%s\nlowered its head!"] = "¡%s\nbajó la cabeza!",
+ ["%s\nis glowing!"] = "¡%s\nestá brillando!",
+ ["The hooked\n%s\nattacked!"] = "¡El %s\nenganchado atacó!",
+ ["Wild %s\nappeared!"] = "¡Un %s\nsalvaje apareció!",
+ ["%s wants\nto fight!"] = "¡%s\nquiere luchar!",
+ ["The GHOST\nappeared!"] = "¡Apareció el\nFANTASMA!",
+ ["Go! %s!"] = "¡Ve, %s!",
+ ["Do it! %s!"] = "¡Hazlo, %s!",
+ ["Get'm! %s!"] = "¡A por él, %s!",
+ ["The enemy's weak!\nGet'm! %s!"] = "¡Está débil!\n¡A por él, %s!",
+ ["%s is out of\nuseable POKéMON!"] = "¡%s no tiene\nPOKéMON útiles!",
+ ["%s blacked\nout!"] = "¡%s se\ndebilitó!",
+ ["%s sent\nout %s!"] = "¡%s envió\na %s!",
+ ["PA: You're out of\nSAFARI BALLs!\nGame over!"] = "AV: ¡No te quedan\nSAFARI BALLs!\n¡Fin del juego!",
+ ["%s is too\nscared to move!"] = "¡%s tiene\ndemasiado miedo!",
+ ["%s has no\nmoves left!"] = "¡%s no tiene\nmovimientos!",
+ ["The move is\ndisabled!"] = "¡El movimiento\nestá anulado!",
+ ["No PP left for\nthis move!"] = "¡No quedan PP para\neste movimiento!",
+ ["But, it failed!"] = "¡Pero falló!",
+ ["%s\nlearned\n%s!"] = "¡%s\naprendió\n%s!",
+ ["POKé BALL"] = "POKé BALL",
+ ["%s used\nPOKé BALL!"] = "¡%s usó\nPOKé BALL!",
+ ["All right!\n%s was\ncaught!"] = "¡Bien!\n¡%s fue\ncapturado!",
+ ["GHOST: Get out...\nGet out..."] = "FANTASMA: Fuera...\nFuera...",
+ ["%s with-\ndrew %s!"] = "¡%s retiró\na %s!",
+ ["%s\nmust recharge!"] = "¡%s debe\nrecargarse!",
+ ["%s\nis fast asleep!"] = "¡%s está\nprofundamente dormido!",
+ ["%s\nis confused!"] = "¡%s está\nconfuso!",
+ ["%s\nwoke up!"] = "¡%s se\ndespertó!",
+ ["%s\nis frozen solid!"] = "¡%s está\ncongelado!",
+ ["%s\ncan't move!"] = "¡%s no\npuede moverse!",
+ ["%s\nflinched!"] = "¡%s se\namedrentó!",
+ ["It hurt itself in\nits confusion!"] = "¡Se hirió a sí\nmismo por confusión!",
+ ["%s\nused %s!"] = "¡%s usó\n%s!",
+ ["%s\nis charging up!"] = "¡%s está\ncargando energía!",
+ ["%s's\nattack missed!"] = "¡El ataque de %s\nfalló!",
+ ["%s's\nattack continues!"] = "¡El ataque de %s\ncontinúa!",
+ ["%s\nis storing energy!"] = "¡%s está\nacumulando energía!",
+ ["%s\nunleashed energy!"] = "¡%s liberó\nsu energía!",
+ ["%s's\nSUBSTITUTE broke!"] = "¡El SUSTITUTO de\n%s se rompió!",
+ ["The SUBSTITUTE\ntook damage for\n%s!"] = "¡El SUSTITUTO\nrecibió el daño\nde %s!",
+ ["%s's\nRAGE is building!"] = "¡La FURIA de %s\nva creciendo!",
+ ["%s\nfainted!"] = "¡%s se\ndebilitó!",
+ ["%s gained\n%d EXP. Points!"] = "¡%s ganó\n%d P. EXP.!",
+ ["%s gained\nwith EXP.ALL,\v%d EXP. Points!"] = "¡%s ganó\ncon EXP.TODOS,\v%d P. EXP.!",
+ ["%s gained\na boosted\v%d EXP. Points!"] = "¡%s ganó\nun extra de\v%d P. EXP.!",
+ ["%s grew\nto level %d!"] = "¡%s subió\nal nivel %d!",
+ ["%s is\nabout to use"] = "%s va a usar",
+ ["%s!"] = "¡%s!",
+ ["Will %s\nchange POKéMON?"] = "¿%s va a\ncambiar de POKéMON?",
+ ["%s defeated\n%s!"] = "¡%s venció\na %s!",
+ ["%s got ¥%d\nfor winning!"] = "¡%s ganó\n¥%d!",
+ ["%s learned\n%s!"] = "¡%s aprendió\n%s!",
+ ["{RIVAL}: Yeah! Am\nI great or what?"] = "{RIVAL}: ¡Sí! ¿Soy\ngenial o qué?",
+ ["Use next POKéMON?"] = "¿Sacar al siguiente?",
+ ["Got away safely!"] = "¡Escapaste!",
+ ["Can't escape!"] = "¡No puedes escapar!",
+ ["There's no will\nto fight!"] = "¡No hay ganas de\nluchar!",
+ ["%s used\nSAFARI BALL!"] = "¡%s usó\nSAFARI BALL!",
+ ["%s threw some\nBAIT."] = "%s echó\nCEBO.",
+ ["%s threw a\nROCK."] = "%s tiró una\nPIEDRA.",
+ ["Wild %s\nis eating!"] = "¡El %s\nsalvaje come!",
+ ["Wild %s\nis angry!"] = "¡El %s\nsalvaje se enfadó!",
+ ["Wild %s\nran!"] = "¡El %s\nsalvaje huyó!",
+ ["No! There's no\nrunning from a\vtrainer battle!"] = "¡No! ¡No puedes\nhuir de un combate\vcontra un entrenador!",
+ ["You missed the\nPOKéMON!"] = "¡Fallaste el tiro!",
+ ["Darn! The POKéMON\nbroke free!"] = "¡Vaya! ¡El POKéMON\nse escapó!",
+ ["Aww! It appeared\nto be caught!"] = "¡Oh! ¡Parecía que\nestaba capturado!",
+ ["Shoot! It was so\nclose too!"] = "¡Vaya! ¡Estuvo\nmuy cerca!",
+ ["Do you want to\ngive a nickname\nto %s?"] = "¿Quieres poner un\nmote a\n%s?",
+ ["NICKNAME?"] = "MOTE?",
+ ["New POKéDEX data\nwill be added for\n%s!"] = "¡Se añadirán datos\nnuevos a la POKéDEX\nde %s!",
+ ["someone's PC"] = "el PC de alguien",
+ ["%s was\ntransferred to\n%s!"] = "¡%s fue\ntransferido a\n%s!",
+ ["But every BOX\nis full!"] = "¡Pero todas las\nCAJAS están llenas!",
+ ["%s used\n%s!"] = "¡%s usó\n%s!",
+ ["The trainer\nblocked the BALL!"] = "¡El entrenador\nbloqueó la BALL!",
+ ["Don't be a thief!"] = "¡No seas ladrón!",
+ ["It dodged the\nthrown BALL!"] = "¡Esquivó la BALL!",
+ ["This POKéMON\ncan't be caught!"] = "¡Este POKéMON no\nse puede capturar!",
+ ["%s is\nalready out!"] = "¡%s ya\nestá fuera!",
+ ["%s picked up\n¥%d!"] = "¡%s recogió\n¥%d!",
+ ["FIGHT"] = "LUCHAR",
+ ["ITEM"] = "OBJETO",
+ ["RUN"] = "HUIR",
+ ["BALLx"] = "BALLx",
+ ["BAIT"] = "CEBO",
+ ["THROW ROCK"] = "TIRAR PIEDRA",
+ ["disabled!"] = "¡anulado!",
+ ["TYPE/"] = "TIPO/",
+ ["It doesn't affect\n%s!"] = "¡No afecta a\n%s!",
+ ["Critical hit!"] = "¡Golpe crítico!",
+ ["One-hit KO!"] = "¡KO en un golpe!",
+ ["It's super\neffective!"] = "¡Es muy eficaz!",
+ ["It's not very\neffective..."] = "¡No es muy\neficaz...",
+ ["Hit the enemy\n%d times!"] = "¡Golpeó al enemigo\n%d veces!",
+ ["Hit %d times!"] = "¡Golpeó %d veces!",
+ ["%s's\nhit with recoil!"] = "¡%s sufrió\nel retroceso!",
+ ["%s is\nprotected by MIST!"] = "¡%s está\nprotegido por NIEBLA!",
+ ["Nothing happened!"] = "¡No pasó nada!",
+ ["%s's\n%s\ngreatly rose!"] = "¡%s\nmejoró mucho su\n%s!",
+ ["%s's\n%s rose!"] = "¡%s mejoró\nsu %s!",
+ ["%s's\n%s fell!"] = "¡%s bajó\nsu %s!",
+ ["%s's\n%s\ngreatly fell!"] = "¡%s\nbajó mucho su\n%s!",
+ ["Fire defrosted\n%s!"] = "¡El fuego descongeló\na %s!",
+ ["%s\nbecame confused!"] = "¡%s se\nconfundió!",
+ ["%s\nwas seeded!"] = "¡%s recibió\nla DRENADORA!",
+ ["%s\nstarted sleeping!"] = "¡%s se\nquedó dormido!",
+ ["%s\nregained health!"] = "¡%s recuperó\nsalud!",
+ ["%s's\nprotected against\nspecial attacks!"] = "¡%s está\nprotegido de los\nataques especiales!",
+ ["%s\ngained armor!"] = "¡%s ganó\narmadura!",
+ ["%s's\nshrouded in mist!"] = "¡%s se\ncubrió de niebla!",
+ ["%s's\ngetting pumped!"] = "¡%s se\nestá animando!",
+ ["All STATUS changes\nare eliminated!"] = "¡Los cambios de\nESTADO desaparecen!",
+ ["%s\nhas a SUBSTITUTE!"] = "¡%s tiene\nun SUSTITUTO!",
+ ["Too weak to make\na SUBSTITUTE!"] = "¡Muy débil para\nhacer un SUSTITUTO!",
+ ["It created a\nSUBSTITUTE!"] = "¡Creó un SUSTITUTO!",
+ ["Converted type to\n%s's!"] = "¡Cambió su tipo al\nde %s!",
+ ["%s\ntransformed into\n%s!"] = "¡%s se\ntransformó en\n%s!",
+ ["%s's\n%s was\ndisabled!"] = "¡El %s\nde %s\nfue anulado!",
+ ["No effect!"] = "¡Sin efecto!",
+ ["Sucked health from\n%s!"] = "¡Absorbió salud de\n%s!",
+ ["%s's\ndream was eaten!"] = "¡Devoró el sueño\nde %s!",
+ ["%s\nkept going and\ncrashed!"] = "¡%s siguió\nadelante y se\nestrelló!",
+ ["Coins scattered\neverywhere!"] = "¡Las monedas se\ndesparramaron!",
+ ["%s\nran away scared!"] = "¡%s huyó\nasustado!",
+ ["%s\nwas blown away!"] = "¡%s salió\nvolando!",
+ ["%s\nran from battle!"] = "¡%s huyó\ndel combate!",
+ ["It didn't affect\n%s!"] = "¡No afectó a\n%s!",
+ ["%s\nis unaffected!"] = "¡%s no se\nvio afectado!",
+ ["The MIRROR MOVE\nfailed!"] = "¡El MOVIMIENTO\nESPEJO falló!",
+ ["%s\nfell asleep!"] = "¡%s se\nquedó dormido!",
+ ["%s\nwas frozen solid!"] = "¡%s se\ncongeló!",
+ ["%s's\nhurt by poison!"] = "¡El veneno hiere a\n%s!",
+ ["%s's\nbadly poisoned!"] = "¡%s está\ngravemente envenenado!",
+ ["%s\nwas poisoned!"] = "¡%s fue\nenvenenado!",
+ ["%s's\nhurt by the burn!"] = "¡La quemadura hiere\na %s!",
+ ["%s\nwas burned!"] = "¡%s se\nquemó!",
+ ["%s's\nfully paralyzed!"] = "¡%s está\ntotalmente paralizado!",
+ ["%s's\nparalyzed! It may\nnot attack!"] = "¡%s está\nparalizado! ¡Puede\nque no ataque!",
+ ["%s's\ndisabled no more!"] = "¡%s ya no\nestá anulado!",
+ ["%s\nsnapped out of\nconfusion!"] = "¡%s salió\nde su confusión!",
+ ["LEECH SEED saps\n%s!"] = "¡La DRENADORA\nabsorbe a %s!",
+ ["%s\nwas afflicted\nby %s!"] = "¡%s sufre\n%s!",
+ ["%s's\nprotected against\nstat changes!"] = "¡%s está\nprotegido de los\ncambios de estado!",
+ ["What will"] = "¿Qué va a hacer",
+ [" do?"] = "?",
+ ["You can't get off\nhere."] = "No puedes bajarte\naquí.",
+ ["%s got off\nthe BICYCLE."] = "%s se bajó\nde la BICICLETA.",
+ ["%s got on\nthe BICYCLE!"] = "¡%s se subió\na la BICICLETA!",
+ ["No cycling\nallowed here."] = "No se puede montar\naquí.",
+ ["No good! It's not\neven near water."] = "¡No sirve! No hay\nagua cerca.",
+ ["OAK: %s!\nThis isn't the\ntime to use that!"] = "OAK: ¡%s!\n¡No es momento\nde usar eso!",
+ ["The TOWN MAP is\nunreadable here."] = "El MAPA PUEBLO no\nse puede leer aquí.",
+ ["Yes! ITEMFINDER\nindicates there's\nan item nearby."] = "¡Sí! El BUSCAOBJ.\nindica que hay algo\ncerca.",
+ ["Nope! ITEMFINDER\nisn't responding."] = "¡No! El BUSCAOBJ.\nno responde.",
+ ["Booted up a TM!"] = "¡Se activó una MT!",
+ ["It contained\n%s!"] = "¡Contenía\n%s!",
+ ["USE"] = "USAR",
+ ["TOSS"] = "TIRAR",
+ ["That's too impor-\ntant to toss!"] = "¡Es demasiado\nimportante!",
+ ["Threw away\n%s."] = "Tiraste\n%s.",
+ ["PRESS A BUTTON"] = "PULSA UN BOTON",
+ ["ESC TO CANCEL"] = "ESC PARA CANCELAR",
+ ["%s :L%d"] = "%s :N%d",
+ ["STATS"] = "DATOS",
+ ["CANCEL"] = "CANCELAR",
+ ["What? There are\nno POKéMON here!"] = "¿Qué? ¡Aquí no hay\nningún POKéMON!",
+ ["You can't take\nany more POKéMON.\fDeposit POKéMON\nfirst."] = "No puedes llevar\nmás POKéMON.\fGuarda alguno\nprimero.",
+ ["BOX %d (WITHDRAW)"] = "CAJA %d (RETIRAR)",
+ ["%s is\ntaken out.\vGot %s."] = "Retirado\n%s.\vRecibes %s.",
+ ["You can't deposit\nthe last POKéMON!"] = "¡No puedes guardar\nel último POKéMON!",
+ ["Oops! This Box is\nfull of POKéMON."] = "¡Uups! Esta CAJA\nestá llena.",
+ ["You need at least\none POKéMON!"] = "¡Necesitas al menos\nun POKéMON!",
+ ["BOX %d is full!"] = "¡La CAJA %d está\nllena!",
+ ["%s was\nstored in Box %s."] = "%s se\nguardó en la CAJA %s.",
+ ["BOX %d (RELEASE)"] = "CAJA %d (SOLTAR)",
+ ["Once released,\n%s is\ngone forever. OK?"] = "Si lo sueltas,\n%s se\nirá para siempre. ¿OK?",
+ ["%s was\nreleased outside.\fBye %s!"] = "%s fue\nliberado.\f¡Adiós, %s!",
+ ["%sBOX %2d"] = "%sCAJA %2d",
+ ["When you change a\nPOKéMON BOX, data\nwill be saved. OK?"] = "Al cambiar de CAJA\nse guardarán los\ndatos. ¿OK?",
+ ["What?"] = "¿Qué?",
+ ["BOX No."] = "CAJA No.",
+ ["BOX No.%d"] = "CAJA No.%d",
+ ["Empty."] = "Vacía.",
+ [":L%d No.%03d"] = ":N%d No.%03d",
+ ["Printed BOX %d!\fSaved as\n%s\vin the save\nfolder."] = "¡CAJA %d impresa!\fGuardada como\n%s\ven la carpeta de\nguardado.",
+ ["Printer error!\n%s"] = "¡Error de impresión!\n%s",
+ ["WITHDRAW "] = "RETIRAR ",
+ ["DEPOSIT "] = "GUARDAR ",
+ ["RELEASE "] = "SOLTAR ",
+ ["CHANGE BOX"] = "CAMBIAR CAJA",
+ ["PRINT BOX"] = "IMPRIMIR CAJA",
+ ["SEE YA!"] = "HASTA LUEGO!",
+ ["YES"] = "SI",
+ ["NO"] = "NO",
+ ["GAME FREAK"] = "",
+ ["Nintendo"] = "",
+ ["Creatures inc."] = "",
+ ["GAME FREAK inc."] = "",
+ ["T H E E N D"] = "F I N",
+ ["HT %d′%02d″"] = "AL %d′%02d″",
+ ["WT %.1flb"] = "PE %.1flb",
+ ["Data unknown."] = "Datos desconocidos.",
+ [""] = "",
+ ["Player"] = "Jugador",
+ ["Huh? %s\nstopped evolving!"] = "¿Eh? ¡%s\ndejó de evolucionar!",
+ ["Congratulations!\nYour %s\nevolved into\n%s!"] = "¡Enhorabuena!\n¡Tu %s\nevolucionó a\n%s!",
+ ["evolving!"] = "evolucionando",
+ ["POKéDEX Seen:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Owned:{NUM:wDexRatingNumMonsOwned, 1, 3}"] = "POKéDEX Vistos:{NUM:wDexRatingNumMonsSeen, 1, 3}\n Capturados:{NUM:wDexRatingNumMonsOwned, 1, 3}",
+ ["POKéDEX Rating{COLON}"] = "Nota POKéDEX{COLON}",
+ ["Keep it up!"] = "¡Sigue así!",
+ ["LEVEL/"] = "NIVEL/",
+ ["TYPE1/"] = "TIPO1/",
+ ["TYPE2/"] = "TIPO2/",
+ ["HALL OF FAME"] = "SALON DE LA FAMA",
+ ["PLAY TIME"] = "TIEMPO",
+ ["MONEY"] = "DINERO",
+ ["bois club games"] = "bois club games",
+ ["GENGAR VS NIDORINO"] = "",
+ ["bois club"] = "bois club",
+ ["Nothing here."] = "Aquí no hay nada.",
+ ["%s is\ntrying to learn\v%s!\fBut, %s\ncan't learn more\vthan 4 moves!\f"] = "¡%s está\nintentando aprender\v%s!\f¡Pero %s\nno puede aprender\vmás de 4!\f",
+ ["Delete an older\nmove to make room\vfor %s?"] = "¿Borrar un movi-\nmiento antiguo para\vaprender %s?",
+ ["HM techniques\ncan't be deleted!"] = "¡Los movimientos MO\nno se pueden borrar!",
+ ["Abandon learning\n%s?"] = "¿Dejar de aprender\n%s?",
+ ["1, 2 and... Poof!\f%s forgot\n%s!\fAnd...\f%s learned\n%s!"] = "¡1, 2 y... plaf!\f¡%s olvidó\n%s!\f¡Y...\f%s aprendió\n%s!",
+ ["%s\ndid not learn\v%s!"] = "¡%s no\naprendió\v%s!",
+ ["Which move should"] = "¿Qué movimiento",
+ ["be forgotten?"] = "hay que olvidar?",
+ ["YOUR NAME?"] = "TU NOMBRE?",
+ ["NEW NAME"] = "NUEVO NOMBRE",
+ ["Hello there!\nWelcome to the\vworld of POKéMON!\fMy name is OAK!\nPeople call me\vthe POKéMON PROF!"] = "¡Hola!\n¡Bienvenido al\vmundo POKéMON!\fMe llamo OAK.\nMe llaman el\vPROF. POKéMON.",
+ ["This world is\ninhabited by\vcreatures called\vPOKéMON!"] = "¡Este mundo está\nhabitado por unas\vcriaturas llamadas\vPOKéMON!",
+ ["\fFor some people,\nPOKéMON are\vpets. Others use\vthem for fights.\fMyself...\fI study POKéMON\nas a profession."] = "\fPara algunos, los\nPOKéMON son masco-\vtas. Otros luchan\vcon ellos.\fYo...\fEstudio los POKéMON\ncomo profesión.",
+ ["{PLAYER}!\fYour very own\nPOKéMON legend is\vabout to unfold!\fA world of dreams\nand adventures\vwith POKéMON\vawaits! Let's go!"] = "¡{PLAYER}!\f¡Tu propia leyenda\nPOKéMON está a\vpunto de comenzar!\f¡Un mundo de sueños\ny aventuras con\vPOKéMON te espera!\v¡Vamos!",
+ ["First, what is\nyour name?"] = "¿Cómo te llamas?",
+ ["This is my grand-\nson. He's been\vyour rival since\vyou were a baby.\f...Erm, what is\nhis name again?"] = "Este es mi nieto.\nHa sido tu rival\vdesde que erais\vbebés.\f...Mmm, ¿cómo se\nllamaba?",
+ ["HIS NAME?"] = "SU NOMBRE?",
+ ["_OakSpeechText2A"] = "",
+ ["TEXT SPEED"] = "VEL TEXTO",
+ ["BATTLE ANIMATION"] = "ANIMACIONES",
+ ["OFF"] = "NO",
+ ["ON"] = "SI",
+ ["BATTLE STYLE"] = "ESTILO COMBATE",
+ ["SET"] = "FIJO",
+ ["SHIFT"] = "CAMBIO",
+ ["BATTLE LAYOUT"] = "DISENO COMBATE",
+ ["WIDE"] = "ANCHO",
+ ["OG"] = "OG",
+ ["RULESET"] = "REGLAS",
+ ["MUSIC VOL"] = "VOL MUSICA",
+ ["SFX VOL"] = "VOL SONIDO",
+ ["PIKACHU VOL"] = "VOL PIKACHU",
+ ["MUSIC FILTER"] = "FILTRO MUSICA",
+ ["COLORS"] = "COLORES",
+ ["TILT"] = "INCLINACION",
+ ["GBC FX"] = "EFECTO GBC",
+ ["ZOOM"] = "ZOOM",
+ ["VOID FILL"] = "RELLENO VACIO",
+ ["VIDEO MODE"] = "MODO VIDEO",
+ ["MAX FPS"] = "FPS MAXIMO",
+ ["GAME SPEED"] = "VELOCIDAD JUEGO",
+ ["MODS"] = "MODS",
+ ["%d INSTALLED"] = "%d INSTALADOS",
+ ["CONTROLS"] = "CONTROLES",
+ ["TOUCH PAD"] = "CONTROL TACTIL",
+ ["SURE? AGAIN"] = "SEGURO? OTRA VEZ",
+ ["AUTO HIDE PAD"] = "OCULTAR AUTO",
+ ["A blinding FLASH\nlights the area!"] = "¡Un DESTELLO\nilumina la zona!",
+ ["No SURFing here!"] = "¡Aquí no se puede\nSURFEAR!",
+ ["Nothing to CUT!"] = "¡Nada que CORTAR!",
+ ["{RAM:wNameBuffer} used\nSTRENGTH."] = "{RAM:wNameBuffer} usó\nFUERZA.",
+ ["{RAM:wNameBuffer} can\nmove boulders."] = "{RAM:wNameBuffer} puede\nmover rocas.",
+ ["It won't have\nany effect."] = "No tendrá ningún\nefecto.",
+ ["%s's HP\nwas restored!"] = "¡Los PS de %s\nse recuperaron!",
+ ["SWITCH"] = "CAMBIAR",
+ ["FLY"] = "VUELO",
+ ["FLASH"] = "DESTELLO",
+ ["CUT"] = "CORTE",
+ ["SURF"] = "SURF",
+ ["STRENGTH"] = "FUERZA",
+ ["SOFTBOILED"] = "HUEVO SUERTE",
+ ["TELEPORT"] = "TELETRANSPORTE",
+ ["DIG"] = "EXCAVAR",
+ ["Use TM on which\nPOKéMON?"] = "¿Usar la MT en qué\nPOKéMON?",
+ ["Bring out which\nPOKéMON?"] = "¿Qué POKéMON\nquieres sacar?",
+ ["Choose a POKéMON."] = "Elige un POKéMON.",
+ ["No POKéMON!"] = "¡Ningún POKéMON!",
+ ["ABLE"] = "PUEDE",
+ ["NOT ABLE"] = "NO PUEDE",
+ ["FNT"] = "DEB",
+ ["Move to where?"] = "¿Mover a dónde?",
+ ["Use on which one?"] = "¿Usar en cuál?",
+ ["You can't carry\nany more items."] = "No puedes llevar\nmás objetos.",
+ ["Withdrew\n%s."] = "Retirado\n%s.",
+ ["No room left to\nstore items."] = "No queda sitio para\nguardar objetos.",
+ ["%s was\nstored via PC."] = "%s se\nguardó en el PC.",
+ ["Toss %s?"] = "¿Tirar %s?",
+ ["Threw away %s."] = "Tiraste %s.",
+ ["WITHDRAW ITEM"] = "RETIRAR OBJETO",
+ ["DEPOSIT ITEM"] = "GUARDAR OBJETO",
+ ["TOSS ITEM"] = "TIRAR OBJETO",
+ ["LOG OFF"] = "SALIR",
+ ["SEEN %d OWNED %d"] = "VISTOS %d CAPT. %d",
+ ["DATA"] = "DATOS",
+ ["CRY"] = "VOZ",
+ ["AREA"] = "ZONA",
+ ["PRNT"] = "IMPR",
+ ["Printed %s's\ndata!\fSaved as\n%s\vin the save\nfolder."] = "¡Datos de %s\nimpresos!\fGuardado como\n%s\ven la carpeta de\nguardado.",
+ ["QUIT"] = "SALIR",
+ ["%s (%s)"] = "%s (%s)",
+ ["%s x%d"] = "%s x%d",
+ ["%s to box %d"] = "%s a caja %d",
+ ["LOAD REPORT"] = "CARGAR PARTIDA",
+ ["A:CONTINUE"] = "A:CONTINUAR",
+ ["You don't have\nenough money."] = "No tienes dinero\nsuficiente.",
+ ["%s?\nThat will be\n¥%d. OK?"] = "¿%s?\nSon ¥%d.\n¿OK?",
+ ["Here you are!\nThank you!"] = "¡Aquí tienes!\n¡Gracias!",
+ ["I can't put a\nprice on that."] = "No puedo ponerle\nprecio a eso.",
+ ["I can pay you\n¥%d for that."] = "Te doy ¥%d\npor eso.",
+ ["BUY"] = "COMPRAR",
+ ["SELL"] = "VENDER",
+ ["%s lined up!\nScored %d coins!"] = "¡%s alineados!\n¡%d fichas!",
+ ["Darn!\nRan out of coins!"] = "¡Vaya!\n¡Sin fichas!",
+ ["Not enough\ncoins!"] = "¡Fichas\ninsuficientes!",
+ ["SLOT MACHINE"] = "MAQUINA TRAGAPERRAS",
+ ["COINS %4d"] = "FICHAS %4d",
+ ["POKéDEX"] = "POKéDEX",
+ ["POKéMON"] = "POKéMON",
+ ["SAVE"] = "GUARDAR",
+ ["PLAYER %s\nBADGES %d\nPOKéDEX %3d\nTIME %6d:%02d"] = "JUGADOR %s\nMEDALLAS %d\nPOKéDEX %3d\nTIEMPO %6d:%02d",
+ ["\fWould you like to\nSAVE the game?"] = "\f¿Quieres GUARDAR\nla partida?",
+ ["Now saving..."] = "Guardando...",
+ ["%s saved\nthe game!"] = "¡%s guardó\nla partida!",
+ ["OPTION"] = "OPCION",
+ ["LINK"] = "LINK",
+ ["RETURN TO MAIN\nMENU?"] = "¿VOLVER AL MENU\nPRINCIPAL?",
+ ["BALL"] = "BALL",
+ ["STATUS/"] = "ESTADO/",
+ ["OT/"] = "EO/",
+ ["EXP POINTS"] = "P. EXP.",
+ ["LEVEL UP"] = "SUBE NIVEL",
+ ["PP"] = "PP",
+ ["SCORE %d"] = "PUNTOS %d",
+ ["New record!"] = "¡Nuevo récord!",
+ ["HI %d"] = "MAX %d",
+ ["A: done"] = "A: listo",
+ ["PLAYER"] = "JUGADOR",
+ ["BADGES"] = "MEDALLAS",
+ ["TIME"] = "TIEMPO",
+ ["CONTINUE"] = "CONTINUAR",
+ ["NEW GAME"] = "NUEVA PARTIDA",
+ ["EXIT GAME"] = "SALIR DEL JUEGO",
+ ["POKéMON RED"] = "",
+ ["2026 bois club games"] = "",
+ ["OT/%s"] = "EO/%s",
+ ["NAME/%s"] = "NOMBRE/%s",
+ ["In battle"] = "En combate",
+ ["Wild battle"] = "Combate salvaje",
+ ["Trainer battle"] = "Combate entrenador",
+ ["Link battle"] = "Combate link",
+ ["Title screen"] = "Pantalla de título",
+ ["Level %d"] = "Nivel %d",
+ ["What?\n%s is\nevolving!\fCongratulations!\nYour %s\nevolved into\n%s!"] = "¿Qué?\n¡%s está\nevolucionando!\f¡Enhorabuena!\n¡Tu %s\nevolucionó a\n%s!",
+ ["Not even a nibble!"] = "¡Ni un mordisco!",
+ ["Oh!\nIt's a bite!"] = "¡Oh!\n¡Ha picado!",
+ ["It's a sculpture\nof DIGLETT."] = "Es una escultura\nde DIGLETT.",
+ ["Crammed full of\nPOKéMON books!"] = "¡Repleto de libros\nsobre POKéMON!",
+ ["There's a slew of\nPOKéMON stuff!"] = "¡Hay un montón de\ncosas POKéMON!",
+ ["An elevator!"] = "¡Un ascensor!",
+ ["INDIGO PLATEAU"] = "MESETA ANIL",
+ ["POKéMON LEAGUE HQ"] = "SEDE DE LA LIGA\nPOKéMON",
+ ["You can't carry\nany more items!"] = "¡No puedes llevar\nmás objetos!",
+ ["%s found\n%s!"] = "¡%s encontró\n%s!",
+ ["%s found\n%d coins!"] = "¡%s encontró\n%d fichas!",
+ ["OUT OF ORDER\nThis is broken."] = "FUERA DE SERVICIO\nEsto está roto.",
+ ["OUT TO LUNCH\nThis is reserved."] = "CERRADO POR COMIDA\nEsto está reservado.",
+ ["Someone's keys!\nThey'll be back."] = "¡Las llaves de\nalguien! Volverá.",
+ ["A COIN CASE is\nrequired!"] = "¡Se necesita un\nMONEDERO!",
+ ["You don't have\nany coins!"] = "¡No tienes fichas!",
+ ["{RAM}\nPOKéMON GYM\nLEADER: {RAM}"] = "{RAM}\nGIMNASIO POKéMON\nLIDER: {RAM}",
+ ["Nope, there's\nonly trash here."] = "No, aquí solo hay\nbasura.",
+ ["Darn! It needs a\nCARD KEY!"] = "¡Vaya! ¡Necesita\nuna LLAVE MAGNET.!",
+ ["Bingo!"] = "¡Bingo!",
+ ["\nThe CARD KEY\nopened the door!"] = "\n¡La LLAVE MAGNET.\nabrió la puerta!",
+ ["Hey! There's a\nswitch under the\ntrash!\fThe 1st electric\nlock opened!"] = "¡Hay un interruptor\nbajo la basura!\f¡Se abrió el 1er\ncierre eléctrico!",
+ ["The 2nd electric\nlock opened!\fThe motorized door\nopened!"] = "¡Se abrió el 2o\ncierre eléctrico!\f¡La puerta se\nabrió!",
+ ["Nope! There's\nonly trash here.\fHey! The electric\nlocks were reset!"] = "¡No! Aquí solo hay\nbasura.\f¡Los cierres se\nreiniciaron!",
+ ["TELEPORTER is\ndisplayed on the\nPC monitor."] = "El TELETRANSPORTE\naparece en el\nmonitor del PC.",
+ ["{PLAYER} initiated\nTELEPORTER's Cell\nSeparator!"] = "¡{PLAYER} activó el\nSeparador de Células\ndel TELETRANSPORTE!",
+ ["BILL's favorite\nPOKéMON list!"] = "¡La lista de POKéMON\nfavoritos de BILL!",
+ ["{PLAYER} got on\n{RAM:wNameBuffer}!"] = "¡{PLAYER} se subió\na {RAM:wNameBuffer}!",
+ ["{RAM:wNameBuffer} hacked\naway with CUT!"] = "¡{RAM:wNameBuffer} cortó\ncon CORTE!",
+ ["Gyaoo!"] = "¡Gyaoo!",
+ ["Hi there!\nMay I help you?"] = "¡Hola!\n¿Puedo ayudarte?",
+ ["SOMEONE'S PC"] = "EL PC DE ALGUIEN",
+ ["PROF.OAK's PC"] = "EL PC DEL PROF.OAK",
+ ["POKéDEX comp-\nletion is:\f{NUM:hDexRatingNumMonsSeen} POKéMON seen\n{NUM:hDexRatingNumMonsOwned} POKéMON owned\fPROF.OAK's\nRating:"] = "La POKéDEX está\nasí:\f{NUM:hDexRatingNumMonsSeen} POKéMON vistos\n{NUM:hDexRatingNumMonsOwned} POKéMON capturados\fNota del\nPROF.OAK:",
+ ["We hope to see\nyou again!"] = "¡Esperamos verte\nde nuevo!",
+ ["Welcome to our\nPOKéMON CENTER!"] = "¡Bienvenido a\nnuestro CENTRO\nPOKéMON!",
+ ["Shall we heal your\nPOKéMON?"] = "¿Curamos a tus\nPOKéMON?",
+ ["OK. We'll need\nyour POKéMON."] = "Bien. Necesitamos\ntus POKéMON.",
+ ["Your POKéMON are\nfighting fit!"] = "¡Tus POKéMON están\nen plena forma!",
+ ["Welcome to the\nCable Club!"] = "¡Bienvenido al Club\nde Cable!",
+ ["We're making\npreparations.\vPlease wait."] = "Estamos preparando\ntodo.\vEspera un momento.",
+ ["Please apply here.\fBefore opening\nthe link, we have\vto save the game."] = "Solicítalo aquí.\fAntes de abrir el\nlink hay que\vguardar la partida.",
+ ["Please come\nagain!"] = "¡Vuelve pronto!",
+ ["I like shorts!\nThey're comfy and\neasy to wear!"] = "¡Me gustan los\npantalones cortos!\n¡Son cómodos!",
+ ["%s received\nthe %s!"] = "¡%s recibió\nel %s!",
+ ["%s received\n%s!"] = "¡%s recibió\n%s!",
+ ["REPEL's effect\nwore off."] = "El efecto del REPEL\nse ha pasado.",
+ ["Go right ahead!"] = "¡Adelante!",
+ ["You don't have the\nBOULDERBADGE yet!"] = "¡Aún no tienes la\nMEDALLA ROCA!",
+ ["Oh! That is the\n{RAM}!"] = "¡Oh! ¡Eso es el\n{RAM}!",
+ ["You don't have the\n{RAM} yet!"] = "¡Aún no tienes el\n{RAM}!",
+ ["You need a\nBICYCLE for the\nCycling Road!"] = "¡Necesitas una\nBICICLETA para el\nCarril Bici!",
+ ["The boulder fell\nthrough the hole!"] = "¡La roca cayó por\nel agujero!",
+ ["PA: Ding-dong!\nTime's up!"] = "AV: ¡Ding-dong!\n¡Se acabó el tiempo!",
+ ["PA: Your SAFARI\nGAME is over!"] = "AV: ¡Tu JUEGO\nSAFARI ha terminado!",
+ ["PA: You're out of\nSAFARI BALLs!"] = "AV: ¡No te quedan\nSAFARI BALLs!",
+ ["{PLAYER} got\n%s!"] = "¡{PLAYER} consiguió\n%s!",
+ ["There's no more\nroom for POKéMON!\v%s was\vsent to POKéMON\vBOX %s on PC!"] = "¡No hay sitio para\nmás POKéMON!\v¡%s fue\venviado a la CAJA\vPOKéMON %s del PC!",
+ ["contribution is not a table"] = "",
+ [" [%s %s.%s]"] = " [%s %s.%s]",
+ ["Link battle needs\nthe same mods on\nboth games."] = "El combate link\nnecesita los mismos\nmods en los dos\njuegos.",
+ ["Your %s can't\nbattle on the\nother game."] = "Tu %s no puede\nluchar en el otro\njuego.",
+ ["Their %s isn't\nin this game.\n(%s)"] = "Su %s no está\nen este juego.\n(%s)",
+ ["%s wants\nto battle!"] = "¡%s quiere\nluchar!",
+ ["Link desync!\n%s differs.\fAre both games\nrunning the same\nmods?"] = "¡Link desincroni-\nzado!\n%s difiere.\f¿Están los dos\njuegos con los\nmismos mods?",
+ ["%s ran from\nthe battle!"] = "¡%s huyó del\ncombate!",
+ ["Items can't be\nused in a link\nbattle!"] = "¡No se pueden usar\nobjetos en un\ncombate link!",
+ ["%s is out of\nPOKéMON!\f%s wins!"] = "¡%s no tiene\nPOKéMON!\f¡%s gana!",
+ ["%s left the\nbattle."] = "%s dejó el\ncombate.",
+ ["%s ran out of\ntime!"] = "¡%s se quedó\nsin tiempo!",
+ ["Time's up! You\nforfeit the match."] = "¡Se acabó el tiempo!\nPierdes el combate.",
+ ["%s's %s can't\nbattle on this\ngame."] = "El %s de %s\nno puede luchar en\neste juego.",
+ ["%s's %s can't\nbattle on this\ngame.\n(%s)"] = "El %s de %s\nno puede luchar en\neste juego.\n(%s)",
+ ["%s vs %s!"] = "¡%s contra %s!",
+ ["Link error:\n%s"] = "Error de link:\n%s",
+ ["Online play runs\nvanilla for both\nplayers.\fTurn off %s\nand restart?"] = "El juego en línea\nva sin mods para\nlos dos jugadores.\f¿Desactivar %s\ny reiniciar?",
+ ["The link was\nbroken."] = "Se ha perdido el\nlink.",
+ ["Link battle\ncan't start."] = "El combate link no\npuede empezar.",
+ ["The trade stopped:\n%s."] = "El intercambio se\ndetuvo:\n%s.",
+ ["The trade was\ncancelled."] = "El intercambio se\nha cancelado.",
+ ["Trade completed!\f%s received\n%s!"] = "¡Intercambio hecho!\f¡%s recibió\n%s!",
+ ["LINK CABLE (LAN)"] = "CABLE LINK (LAN)",
+ ["ONLINE MATCH"] = "PARTIDA EN LINEA",
+ ["TOURNAMENT"] = "TORNEO",
+ ["HOST A GAME"] = "CREAR PARTIDA",
+ ["JOIN A GAME"] = "UNIRSE A PARTIDA",
+ ["UDP port %s"] = "Puerto UDP %s",
+ ["HOST ONLINE"] = "CREAR EN LINEA",
+ ["JOIN ONLINE"] = "UNIRSE EN LINEA",
+ ["Tell your friend"] = "Dile a tu amigo",
+ ["the code:"] = "el código:",
+ ["Waiting for join..."] = "Esperando...",
+ ["A: connect B: back"] = "A: conectar B: atrás",
+ ["Calling..."] = "Llamando...",
+ ["Friend joins at:"] = "Tu amigo entra en:",
+ ["Port: %s"] = "Puerto: %s",
+ ["TRADE"] = "INTERCAMBIO",
+ ["BATTLE"] = "COMBATE",
+ ["LEVELS:"] = "NIVELES:",
+ ["A: continue B: back"] = "A: seguir B: atrás",
+ ["Checking the"] = "Comprobando el",
+ ["other game..."] = "otro juego...",
+ ["Waiting for the"] = "Esperando a que",
+ ["host to choose..."] = "el anfitrión elija...",
+ ["A: trade anyway"] = "A: intercambiar igual",
+ ["YOURS"] = "TUYO",
+ ["THEIRS"] = "SUYO",
+ ["X: not on theirs"] = "X: no en el suyo",
+ ["A: trade B: cancel"] = "A: cambiar B: cancelar",
+ ["Exchanging data..."] = "Intercambiando...",
+ ["can't reach relay %s:%d\n(%s)"] = "",
+ ["That code wasn't\nfound."] = "Ese código no se\nha encontrado.",
+ ["That game already\nhas two players."] = "Esa partida ya\ntiene dos jugadores.",
+ ["That code has\nexpired."] = "Ese código ha\ncaducado.",
+ ["Couldn't join:\n%s"] = "No se pudo unir:\n%s",
+ ["no answer from\n%s"] = "",
+ ["That tournament\nhas already begun."] = "Ese torneo ya ha\nempezado.",
+ ["Can't host:\nneed %d Pokemon\nLv %s-%s."] = "No puedes crearlo:\nnecesitas %d Pokemon\nNv %s-%s.",
+ ["Couldn't host\nthat tournament."] = "No se pudo crear\nese torneo.",
+ ["Your party needs\n%d Pokemon, Lv\n%s-%s."] = "Tu equipo necesita\n%d Pokemon, Nv\n%s-%s.",
+ ["Couldn't join\nthat tournament."] = "No se pudo unir a\nese torneo.",
+ ["Link error:\nversion mismatch\nwith opponent."] = "Error de link:\nversión distinta a\nla del rival.",
+ ["The tournament\nconnection was\nlost."] = "Se perdió la\nconexión del torneo.",
+ ["Can't watch this\nmatch."] = "No se puede ver\neste combate.",
+ ["HOST"] = "CREAR",
+ ["JOIN"] = "UNIRSE",
+ ["START: create"] = "START: crear",
+ ["A: join B: back"] = "A: unirse B: atrás",
+ ["B: cancel"] = "B: cancelar",
+ ["TOURNAMENT %s"] = "TORNEO %s",
+ ["ROUND %d"] = "RONDA %d",
+ ["%s (bye)"] = "%s (pasa)",
+ ["%s%s vs %s%s"] = "%s%s contra %s%s",
+ ["(organizing --"] = "(organizando --",
+ ["not playing)"] = "no juega)",
+ ["Waiting for"] = "Esperando a que",
+ ["players to join:"] = "entren jugadores:",
+ ["A: START B: cancel"] = "A: START B: cancelar",
+ ["%s is the"] = "¡%s es el",
+ ["champion!"] = "campeón!",
+ ["A: continue"] = "A: continuar",
+ ["{PLAYER} played the\nPOKé FLUTE."] = "{PLAYER} tocó la\nFLAUTA POKé.",
+ ["Played the POKé\nFLUTE.\fNow, that's a\ncatchy tune!"] = "Tocaste la FLAUTA\nPOKé.\f¡Qué melodía tan\npegadiza!",
+ ["%s played the\nPOKé FLUTE."] = "%s tocó la\nFLAUTA POKé.",
+ ["All sleeping\nPOKéMON woke up!"] = "¡Todos los POKéMON\ndormidos despertaron!",
+ ["%s's\nhits will never\nmiss!"] = "¡Los golpes de %s\nnunca fallarán!",
+ ["The wild POKéMON\nran away!"] = "¡El POKéMON salvaje\nhuyó!",
+ ["%s's PP\nwas restored!"] = "¡Los PP de %s\nse recuperaron!",
+ ["%s's\nstatus returned\nto normal!"] = "¡El estado de %s\nvolvió a la\nnormalidad!",
+ ["%s\nis revitalized!"] = "¡%s se ha\nrevitalizado!",
+ ["%s\nis refusing!"] = "¡%s se\nniega!",
+ ["%s's %s\nrose!"] = "¡El %s de %s\nsubió!",
+ ["%s's PP\nincreased!"] = "¡Los PP de %s\naumentaron!",
+ ["%s can't\nlearn that move!"] = "¡%s no puede\naprender ese\nmovimiento!",
+ ["It knows that\nmove already!"] = "¡Ya conoce ese\nmovimiento!",
+ ["Coin count:\n%d"] = "Fichas:\n%d",
+ ["NO MODS INSTALLED"] = "NO HAY MODS",
+ ["SAVE CURRENT AS.."] = "GUARDAR ACTUAL..",
+ ["OPTIONS.."] = "OPCIONES..",
+ ["PERMISSIONS.."] = "PERMISOS..",
+ ["VIEW ERROR.."] = "VER ERROR..",
+ ["BACK"] = "ATRAS",
+ ["APPLY & RESTART"] = "APLICAR Y REINICIAR",
+ ["DISCARD CHANGES"] = "DESCARTAR CAMBIOS",
+ ["DATA & API ONLY"] = "SOLO DATOS Y API",
+ ["DISABLE BOTH?"] = "DESACTIVAR AMBOS?",
+ ["PROFILE NAME?"] = "NOMBRE DEL PERFIL?",
+ ["RENAME?"] = "RENOMBRAR?",
+ ["RESET DEFAULTS"] = "VALORES POR DEFECTO",
+ ["NO CHANGES"] = "SIN CAMBIOS",
+ ["A:OK"] = "A:OK",
+ ["B:DONE (NO RESTART)"] = "B:LISTO (SIN REINICIAR)",
+ ["MOD MANAGER"] = "GESTOR DE MODS",
+ ["Choose a mod .zip"] = "Elige un .zip de mod",
+ ["Choose a .sav save file"] = "Elige un archivo .sav",
+ ["An update is available"] = "Hay una actualización",
+ ["Name save slot"] = "Nombra la ranura",
+ ["Enter to save - Esc to cancel - empty clears"] = "Enter para guardar - Esc para cancelar - vacío la borra",
+ ["Add a mod index"] = "Añadir un índice de mods",
+ ["Paste the index URL, or its owner/repo."] = "Pega la URL del índice, o su owner/repo.",
+ ["Enter to add - Esc to cancel"] = "Enter para añadir - Esc para cancelar",
+ ["Import a ROM to play"] = "Importa una ROM para jugar",
+ ["RED"] = "ROJO",
+ ["BLUE"] = "AZUL",
+ ["YELLOW"] = "AMARILLO",
+ ["FIND MODS"] = "BUSCAR MODS",
+ ["%d of 3 ready"] = "%d de 3 listos",
+ ["Or drop the .gb/.gbc file here."] = "O arrastra aquí el archivo .gb/.gbc.",
+ ["ROM imported"] = "ROM importada",
+ ["That ROM could not be imported."] = "No se pudo importar esa ROM.",
+ ["Open folder"] = "Abrir carpeta",
+ ["%d badges - %s - %d caught"] = "%d medallas - %s - %d capturados",
+ ["%d of %d enabled"] = "%d de %d activados",
+ ["Or drop a mod .zip onto the window."] = "O arrastra un .zip de mod a la ventana.",
+ ["No mods installed - drop a mod .zip here to add one."] = "No hay mods - arrastra aquí un .zip para añadir uno.",
+ ["Refreshed - %d mods listed"] = "Actualizado - %d mods listados",
+ ["Added %s"] = "Añadido %s",
+ ["Index removed"] = "Índice eliminado",
+ ["Downloading %s..."] = "Descargando %s...",
+ ["Installed %s %s"] = "Instalado %s %s",
+ ["%d mods listed"] = "%d mods listados",
+ ["%d of %d mods"] = "%d de %d mods",
+ ["Mods here are listed, not reviewed - read the source and trust the author."] = "Los mods aquí se listan, no se revisan - lee el código y confía en el autor.",
+ ["No mod index added"] = "No hay índice de mods",
+ ["Add an index to browse mods. An index is a published list; paste its URL or its owner/repo."] = "Añade un índice para explorar mods. Un índice es una lista publicada; pega su URL o su owner/repo.",
+ ["Search mods"] = "Buscar mods",
+ ["This index lists no mods yet."] = "Este índice aún no lista mods.",
+ ["No mods match that search."] = "Ningún mod coincide con esa búsqueda.",
+}
diff --git a/mods/spanish_ui/lang/trainer_names.lua b/mods/spanish_ui/lang/trainer_names.lua
new file mode 100644
index 00000000..c6537732
--- /dev/null
+++ b/mods/spanish_ui/lang/trainer_names.lua
@@ -0,0 +1,7 @@
+-- Trainer class names
+--
+-- Trainer class names for Espanol.
+
+return {
+ ["OPP_FIX_YOUNGSTER"] = "",
+}
diff --git a/mods/spanish_ui/main.lua b/mods/spanish_ui/main.lua
new file mode 100644
index 00000000..ada371d1
--- /dev/null
+++ b/mods/spanish_ui/main.lua
@@ -0,0 +1,127 @@
+-- spanish_ui: a translation of the game into Espanol.
+--
+-- Nothing here is translated yet. Every table under lang/ starts with
+-- empty strings; fill one in and it takes effect on the next boot, and
+-- anything still empty keeps rendering in English. That means a
+-- half-finished translation is always playable, so you can ship early and
+-- fill the long tail in later.
+--
+-- Read TRANSLATING.md before the first edit; the font is the part people
+-- get wrong.
+return function(mod)
+ -- mod:read is the supported way into your own directory; the catalogs are
+ -- plain Lua tables, so read and run them rather than require()ing them.
+ local function catalog(name)
+ local rel = "lang/" .. name .. ".lua"
+ local body = mod:read(rel)
+ if not body then return {} end
+ local chunk, err = loadstring(body, rel)
+ if not chunk then
+ mod.log:warn("%s has a syntax error: %s", rel, tostring(err))
+ return {}
+ end
+ local ok, table_ = pcall(chunk)
+ if not ok or type(table_) ~= "table" then
+ mod.log:warn("%s did not return a table: %s", rel, tostring(table_))
+ return {}
+ end
+ return table_
+ end
+
+ -- An empty value means "not translated yet", never "translate to blank".
+ local function each(name, apply)
+ local n = 0
+ for key, value in pairs(catalog(name)) do
+ if type(value) == "string" and value ~= "" then
+ apply(key, value)
+ n = n + 1
+ end
+ end
+ return n
+ end
+
+ -- ---- glyphs -------------------------------------------------------
+ -- Register the sheet BEFORE anything asks for a glyph on it. base is
+ -- the first code the page owns; 0x100 and up is free space above the
+ -- vanilla pages, so a new alphabet never collides with them.
+ for id, page in pairs(catalog("font")) do
+ mod.content.font:register(id, page)
+ end
+ -- charmap: which byte sequence draws which code
+ for seq, code in pairs(catalog("charmap")) do
+ mod.content.font:register("charmap:" .. seq, { seq = seq, code = code })
+ end
+
+ -- ---- text ---------------------------------------------------------
+ local counts = {}
+ counts.dialogue = each("dialogue", function(id, value)
+ mod.content.text:override(id, value)
+ end)
+ counts.strings = each("strings", function(source, value)
+ mod.content.strings:override(source, value)
+ end)
+ counts.species = each("species_names", function(id, value)
+ mod.content.pokemon:patch(id, { name = value })
+ end)
+ counts.moves = each("move_names", function(id, value)
+ mod.content.moves:patch(id, { name = value })
+ end)
+ counts.items = each("item_names", function(id, value)
+ mod.content.items:patch(id, { name = value })
+ end)
+ counts.trainers = each("trainer_names", function(id, value)
+ mod.content.trainers:patch(id, { name = value })
+ end)
+ counts.statuses = each("status_labels", function(id, value)
+ mod.content.statuses:patch(id, { label = value })
+ end)
+
+ -- ---- name entry ---------------------------------------------------
+ -- The naming screen's letter grid. Leave lang/naming.lua returning nil
+ -- to keep the English alphabet.
+ local grid = catalog("naming")
+ if grid.upper then
+ -- Only offer the accented cells when the running cartridge can actually
+ -- draw them. A Spanish ROM has Ñ and the accented vowels in its font
+ -- and the manifest maps them; an English one does not, and an
+ -- unmappable cell renders blank -- a naming screen with six empty keys
+ -- is worse than an English one. So check the charmap and fall back.
+ local function drawable(cells, ctx)
+ local font = ((ctx.game or {}).data or {}).font
+ local charmap = font and font.charmap
+ if not charmap then return false end
+ local have = {}
+ for _, entry in ipairs(charmap) do have[entry.seq] = true end
+ for _, row in ipairs(cells) do
+ for _, cell in ipairs(row) do
+ -- Only the non-ASCII cells are at risk; A-Z and punctuation are
+ -- on every page.
+ if cell:byte(1) and cell:byte(1) > 127 and not have[cell] then
+ return false
+ end
+ end
+ end
+ return true
+ end
+ local warned = false
+ mod.hooks:on("ui.naming.grid", function(base, ctx)
+ local want = ctx.lower and grid.lower or grid.upper
+ if not want then return base end
+ if not drawable(want, ctx) then
+ if not warned then
+ warned = true
+ mod.log:info("naming grid: this ROM has no accented glyphs, "
+ .. "keeping the English alphabet")
+ end
+ return base
+ end
+ return want
+ end)
+ end
+
+ mod.events:on("game.ready", function()
+ local total = 0
+ for _, n in pairs(counts) do total = total + n end
+ mod.log:info("Espanol: %d strings translated", total)
+ end)
+end
diff --git a/mods/spanish_ui/manifest.json b/mods/spanish_ui/manifest.json
new file mode 100644
index 00000000..b3e45f04
--- /dev/null
+++ b/mods/spanish_ui/manifest.json
@@ -0,0 +1,17 @@
+{
+ "id": "spanish_ui",
+ "name": "Espanol (interfaz)",
+ "version": "0.1.0",
+ "api": 2,
+ "entry": "main.lua",
+ "profile": "content",
+ "game_version": ">=0.0.0-dev <1.0.0",
+ "category": "LANGUAGE",
+ "priority": 100,
+ "dependencies": [],
+ "optional_dependencies": [],
+ "conflicts": [],
+ "incompatible": [],
+ "experimental": false,
+ "description": "Spanish for the app's own settings and menus. The game's text comes from your ROM and is untouched, so an English cartridge stays an English adventure with Spanish menus."
+}
\ No newline at end of file
diff --git a/assets/switch/icon.jpg b/ports/switch/assets/icon.jpg
similarity index 100%
rename from assets/switch/icon.jpg
rename to ports/switch/assets/icon.jpg
diff --git a/ports/switch/assets/logo.rgba b/ports/switch/assets/logo.rgba
new file mode 100644
index 00000000..cb97707a
Binary files /dev/null and b/ports/switch/assets/logo.rgba differ
diff --git a/ports/switch/ota-bootstrap/Makefile b/ports/switch/ota-bootstrap/Makefile
new file mode 100644
index 00000000..4920b3c3
--- /dev/null
+++ b/ports/switch/ota-bootstrap/Makefile
@@ -0,0 +1,72 @@
+#---------------------------------------------------------------------------------
+# One-shot OTA bootstrap NRO (copied into the main launcher's romfs).
+#---------------------------------------------------------------------------------
+.SUFFIXES:
+
+ifeq ($(strip $(DEVKITPRO)),)
+
+.PHONY: all
+all:
+ @echo "DEVKITPRO not set — bootstrap builds with the launcher only."
+ @false
+
+else
+
+TOPDIR ?= $(CURDIR)
+include $(DEVKITPRO)/libnx/switch_rules
+
+TARGET := ota-bootstrap
+BUILD := build
+SOURCES := .
+INCLUDES := include
+ROMFS :=
+
+APP_TITLE := gen1recomp OTA
+APP_AUTHOR := bryanthaboi, port by andrewqsantos
+APP_VERSION := 0.0.0
+
+ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
+CFLAGS := -g -Wall -O2 -ffunction-sections $(ARCH) $(DEFINES)
+CFLAGS += $(INCLUDE) -D__SWITCH__
+ASFLAGS := -g $(ARCH)
+LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
+LIBS := -lnx
+LIBDIRS := $(LIBNX)
+
+ifneq ($(BUILD),$(notdir $(CURDIR)))
+export OUTPUT := $(CURDIR)/$(TARGET)
+export TOPDIR := $(CURDIR)
+export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir))
+export DEPSDIR := $(CURDIR)/$(BUILD)
+CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
+export LD := $(CC)
+export OFILES := $(CFILES:.c=.o)
+export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
+ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \
+ -I$(CURDIR)/$(BUILD)
+export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
+export APP_ICON := $(LIBNX)/default_icon.jpg
+export NROFLAGS += --icon=$(APP_ICON)
+export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
+
+.PHONY: $(BUILD) clean all
+
+all: $(BUILD)
+
+$(BUILD):
+ @[ -d $@ ] || mkdir -p $@
+ @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
+
+clean:
+ @echo clean ...
+ @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf
+
+else
+.PHONY: all
+DEPENDS := $(OFILES:.o=.d)
+all: $(OUTPUT).nro
+$(OUTPUT).nro: $(OUTPUT).elf $(OUTPUT).nacp
+$(OUTPUT).elf: $(OFILES)
+-include $(DEPENDS)
+endif
+endif
diff --git a/ports/switch/ota-bootstrap/main.c b/ports/switch/ota-bootstrap/main.c
new file mode 100644
index 00000000..0aec9352
--- /dev/null
+++ b/ports/switch/ota-bootstrap/main.c
@@ -0,0 +1,36 @@
+/*
+ * Tiny one-shot helper: swap a staged launcher NRO into place, then load the game.
+ * The main OTA launcher chainloads here because a running NRO cannot replace itself
+ * on sdmc/FAT.
+ */
+#include
+#include
+#include
+
+#if defined(__SWITCH__)
+#include
+#endif
+
+#define INSTALL_DIR "sdmc:/switch/gen1recomp"
+#define STAGED INSTALL_DIR "/gen1recomp.nro.staged"
+#define LAUNCHER INSTALL_DIR "/gen1recomp.nro"
+#define GAME INSTALL_DIR "/gen1recomp-game.nro"
+
+int main(int argc, char **argv) {
+ (void)argc;
+ (void)argv;
+
+#if defined(__SWITCH__)
+ remove(LAUNCHER);
+ if (rename(STAGED, LAUNCHER) != 0) {
+ return 1;
+ }
+ Result rc = envSetNextLoad(GAME, GAME);
+ if (R_FAILED(rc)) return 1;
+ return 0;
+#else
+ fprintf(stderr, "ota-bootstrap: host stub (would rename %s -> %s, load %s)\n", STAGED, LAUNCHER,
+ GAME);
+ return 0;
+#endif
+}
diff --git a/ports/switch/ota-launcher/Makefile b/ports/switch/ota-launcher/Makefile
new file mode 100644
index 00000000..2f2c710d
--- /dev/null
+++ b/ports/switch/ota-launcher/Makefile
@@ -0,0 +1,193 @@
+#---------------------------------------------------------------------------------
+# gen1recomp Switch native OTA launcher
+# Requires: DEVKITPRO + switch-curl, switch-mbedtls, switch-zlib, switch-zziplib
+# Host protocol tests: make host-test (no DEVKITPRO needed)
+#---------------------------------------------------------------------------------
+.SUFFIXES:
+
+#---------------------------------------------------------------------------------
+# Host-only path when DEVKITPRO is unset
+#---------------------------------------------------------------------------------
+ifeq ($(strip $(DEVKITPRO)),)
+
+.PHONY: host-test clean-host
+host-test:
+ @mkdir -p build-host
+ cc -std=c11 -Wall -Wextra -Iinclude -o build-host/test_ota_protocol \
+ src/ota_protocol.c host/test_ota_protocol.c
+ ./build-host/test_ota_protocol
+
+clean-host:
+ rm -rf build-host
+
+%:
+ @echo "DEVKITPRO not set — only 'make host-test' is available."
+ @echo "Run: bash scripts/switch/install_devkitpro_deps.sh"
+ @false
+
+else
+
+#---------------------------------------------------------------------------------
+TOPDIR ?= $(CURDIR)
+include $(DEVKITPRO)/libnx/switch_rules
+
+#---------------------------------------------------------------------------------
+TARGET := gen1recomp
+BUILD := build
+SOURCES := src
+DATA := data
+INCLUDES := include
+ROMFS := romfs
+
+APP_TITLE := gen1recomp
+APP_AUTHOR := bryanthaboi, port by andrewqsantos
+# Overridable: scripts/switch/build_ota_launcher.sh passes release X.Y.Z
+APP_VERSION ?= 0.0.0
+
+# Prefer project Switch icon if present
+ICON := $(TOPDIR)/../assets/icon.jpg
+LOGO_RGBA_SRC := $(TOPDIR)/../assets/logo.rgba
+LOGO_ROMFS := $(CURDIR)/$(ROMFS)/logo.rgba
+
+#---------------------------------------------------------------------------------
+ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
+
+CFLAGS := -g -Wall -O2 -ffunction-sections $(ARCH) $(DEFINES)
+CFLAGS += $(INCLUDE) -D__SWITCH__
+
+CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
+
+ASFLAGS := -g $(ARCH)
+LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
+
+LIBS := -lcurl -lzzip -lmbedtls -lmbedx509 -lmbedcrypto -lz -lnx
+
+LIBDIRS := $(PORTLIBS) $(LIBNX)
+
+#---------------------------------------------------------------------------------
+ifneq ($(BUILD),$(notdir $(CURDIR)))
+#---------------------------------------------------------------------------------
+
+export OUTPUT := $(CURDIR)/$(TARGET)
+export TOPDIR := $(CURDIR)
+
+export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
+ $(foreach dir,$(DATA),$(CURDIR)/$(dir))
+
+export DEPSDIR := $(CURDIR)/$(BUILD)
+
+CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
+CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
+SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
+BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
+
+#---------------------------------------------------------------------------------
+# use CXX for linking C++ projects, CC for standard C
+#---------------------------------------------------------------------------------
+ifeq ($(strip $(CPPFILES)),)
+ export LD := $(CC)
+else
+ export LD := $(CXX)
+endif
+#---------------------------------------------------------------------------------
+
+export OFILES_BIN := $(addsuffix .o,$(BINFILES))
+export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
+export OFILES := $(OFILES_BIN) $(OFILES_SRC)
+
+export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
+ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \
+ -I$(CURDIR)/$(BUILD)
+
+export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
+
+ifeq ($(strip $(ICON)),)
+ icons := $(wildcard *.jpg)
+ ifneq (,$(findstring $(TARGET).jpg,$(icons)))
+ export APP_ICON := $(TOPDIR)/$(TARGET).jpg
+ else
+ ifneq (,$(findstring icon.jpg,$(icons)))
+ export APP_ICON := $(TOPDIR)/icon.jpg
+ endif
+ endif
+else
+ ifeq ($(wildcard $(ICON)),)
+ # fall back to libnx default if project icon missing
+ export APP_ICON := $(LIBNX)/default_icon.jpg
+ else
+ export APP_ICON := $(ICON)
+ endif
+endif
+
+ifeq ($(strip $(NO_ICON)),)
+ export NROFLAGS += --icon=$(APP_ICON)
+endif
+
+ifeq ($(strip $(NO_NACP)),)
+ export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
+endif
+
+ifneq ($(ROMFS),)
+ export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
+endif
+
+BOOTSTRAP_DIR := $(TOPDIR)/../ota-bootstrap
+BOOTSTRAP_ROMFS := $(CURDIR)/$(ROMFS)/ota-bootstrap.nro
+
+.PHONY: $(BUILD) clean all host-test sync-romfs bootstrap-romfs
+
+#---------------------------------------------------------------------------------
+all: sync-romfs $(BUILD)
+
+CACERT_URL := https://curl.se/ca/cacert.pem
+CACERT_ROMFS := $(CURDIR)/$(ROMFS)/cacert.pem
+
+bootstrap-romfs:
+ @$(MAKE) --no-print-directory -C $(BOOTSTRAP_DIR) all
+ @mkdir -p $(CURDIR)/$(ROMFS)
+ @cp -f $(BOOTSTRAP_DIR)/ota-bootstrap.nro $(BOOTSTRAP_ROMFS)
+
+sync-romfs: bootstrap-romfs
+ @mkdir -p $(CURDIR)/$(ROMFS)
+ @[ -f "$(LOGO_RGBA_SRC)" ] || (echo "missing $(LOGO_RGBA_SRC) — run: python3 scripts/switch/bake_ota_logo.py" && exit 1)
+ @cp -f "$(LOGO_RGBA_SRC)" "$(LOGO_ROMFS)"
+ @if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \
+ [ -f $(CACERT_ROMFS) ] || (echo "sync-romfs: failed to fetch cacert.pem" && exit 1); \
+ fi
+
+$(BUILD): sync-romfs
+ @[ -d $@ ] || mkdir -p $@
+ @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
+
+#---------------------------------------------------------------------------------
+clean:
+ @echo clean ...
+ @$(MAKE) --no-print-directory -C $(BOOTSTRAP_DIR) clean || true
+ @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst build-host
+ @rm -f $(CURDIR)/$(ROMFS)/logo.rgba $(CURDIR)/$(ROMFS)/logo.png
+
+host-test:
+ @mkdir -p build-host
+ cc -std=c11 -Wall -Wextra -Iinclude -o build-host/test_ota_protocol \
+ src/ota_protocol.c host/test_ota_protocol.c
+ ./build-host/test_ota_protocol
+
+#---------------------------------------------------------------------------------
+else
+.PHONY: all
+
+DEPENDS := $(OFILES:.o=.d)
+
+#---------------------------------------------------------------------------------
+all: $(OUTPUT).nro
+
+$(OUTPUT).nro: $(OUTPUT).elf $(OUTPUT).nacp
+
+$(OUTPUT).elf: $(OFILES)
+
+-include $(DEPENDS)
+
+#---------------------------------------------------------------------------------
+endif
+#---------------------------------------------------------------------------------
+endif
diff --git a/ports/switch/ota-launcher/README.md b/ports/switch/ota-launcher/README.md
new file mode 100644
index 00000000..f6865955
--- /dev/null
+++ b/ports/switch/ota-launcher/README.md
@@ -0,0 +1,89 @@
+# Native Switch OTA launcher
+
+In-console updates for Gen1Recomp on Nintendo Switch. This NRO is the **hbmenu
+entry** (`gen1recomp.nro`). It checks GitHub Releases quietly (no UI when you
+are already up to date or offline). Only if a newer release exists does it show
+a **launcher-style screen** (black + RGB rail + project logo + flat A/B
+buttons), download the same `gen1recomp-*-switch.zip` used for install, verify
+SHA-256 from `sha256sums.txt`, replace **both** `gen1recomp-game.nro` and
+`gen1recomp.nro` (matching NACP version for hbmenu/Sphaira), then load the game
+with `envSetNextLoad`.
+
+The LÖVE self-updater (`src/update/Check.lua`) stays **disabled** on NX.
+Wire format (also in Lua): `src/update/SwitchOta.lua`.
+NACP icon: `ports/switch/assets/icon.jpg`.
+
+## Layout on microSD
+
+```text
+sdmc:/switch/gen1recomp/gen1recomp.nro <- this launcher
+sdmc:/switch/gen1recomp/gen1recomp-game.nro <- fused LÖVE game
+sdmc:/switch/gen1recomp/version.txt <- installed X.Y.Z
+sdmc:/switch/gen1recomp/pokemon-love2d/ <- saves (never touched by OTA)
+```
+
+## Host tests (no DEVKITPRO)
+
+```bash
+cd ports/switch/ota-launcher
+make host-test
+```
+
+## Switch build (DEVKITPRO)
+
+Needs `DEVKITPRO` with packages roughly:
+
+```bash
+(dkp-)pacman -S --needed switch-curl switch-mbedtls switch-zlib switch-zziplib
+# or: bash scripts/switch/install_devkitpro_deps.sh
+```
+
+```bash
+export DEVKITPRO=/opt/devkitpro # typical
+cd ports/switch/ota-launcher
+make
+# -> gen1recomp.nro
+```
+
+Or from repo root (as part of `--fused`):
+
+```bash
+scripts/build_switch.sh --fetch --fused --version X.Y.Z
+```
+
+Standalone launcher build:
+
+```bash
+scripts/switch/build_ota_launcher.sh
+```
+
+Docker fallback uses the same pin as fused builds (`scripts/switch/dkp-docker.image`).
+
+## OTA logo asset
+
+The launcher draws a pre-scaled logo from `romfs:/logo.rgba` (no PNG decoder in
+the NRO). The baked blob lives at `../assets/logo.rgba` and is copied into romfs at
+build time. After changing `assets/logo/logo.png`, regenerate:
+
+```bash
+python3 scripts/switch/bake_ota_logo.py
+```
+
+Requires Pillow, or on macOS uses `sips` when Pillow is not installed.
+
+## Packaging
+
+`scripts/switch/pack_sd_zip.sh GAME_NRO VERSION OUT_ZIP LAUNCHER_NRO` writes both
+NROs into the SD zip. That zip is also what OTA downloads.
+Manifest: `scripts/switch/ota_launcher.manifest`.
+
+## Status / known gaps
+
+- Zip extraction uses `switch-zziplib` (`ota_unzip.c`) on device.
+- OTA replaces game + launcher from the install zip (NACP versions stay aligned).
+ The running launcher cannot overwrite its own NRO on sdmc/FAT; a tiny
+ `ota-bootstrap.nro` (embedded in romfs) chainloads once to swap the staged
+ launcher, then loads the game.
+- HTTPS uses Mozilla CA bundle in romfs (`cacert.pem`, fetched at build time). `ota_net_init()` mounts romfs before the quiet release check.
+- Sphaira HOME forwarders cache metadata until reinstalled (see docs/switch-install.md).
+- Release runner: `switch-dev` + (`install_devkitpro_deps.sh` **or** Docker)
diff --git a/ports/switch/ota-launcher/host/test_ota_protocol.c b/ports/switch/ota-launcher/host/test_ota_protocol.c
new file mode 100644
index 00000000..2e432f2a
--- /dev/null
+++ b/ports/switch/ota-launcher/host/test_ota_protocol.c
@@ -0,0 +1,144 @@
+/* Host-only unit tests for ota_protocol.c — no libnx. */
+#include "ota_protocol.h"
+
+#include
+#include
+
+static int g_fail = 0;
+
+static void expect(int cond, const char *msg) {
+ if (!cond) {
+ fprintf(stderr, "FAIL: %s\n", msg);
+ g_fail++;
+ } else {
+ printf("PASS: %s\n", msg);
+ }
+}
+
+int main(void) {
+ expect(ota_compare_semver("1.2.0", "1.1.0") == 1, "semver newer");
+ expect(ota_compare_semver("1.1.0", "1.1.0") == 0, "semver equal");
+ expect(ota_compare_semver("1.0.0", "1.1.0") == -1, "semver older");
+ expect(ota_is_ota_asset_name("gen1recomp-1.5.0-switch.zip"), "ota asset name");
+ expect(!ota_is_ota_asset_name("gen1recomp-1.5.0-switch-ota.zip"), "reject legacy ota zip name");
+ expect(!ota_is_ota_asset_name("gen1recomp-1.5.0.love"), "reject love payload");
+
+ const char *json =
+ "{"
+ "\"tag_name\":\"v1.5.0\","
+ "\"assets\":["
+ "{\"name\":\"gen1recomp-1.5.0-switch.zip\","
+ "\"browser_download_url\":\"https://example/switch.zip\"},"
+ "{\"name\":\"sha256sums.txt\",\"browser_download_url\":\"https://example/sums\"}"
+ "]"
+ "}";
+ ota_release_t rel;
+ expect(ota_parse_release(json, &rel) == 1, "parse release");
+ expect(strcmp(rel.version, "1.5.0") == 0, "release version");
+ expect(strcmp(rel.asset_name, "gen1recomp-1.5.0-switch.zip") == 0, "release asset");
+
+ ota_decision_t d;
+ ota_decide_update("1.4.0", &rel, &d);
+ expect(strcmp(d.status, "available") == 0, "decide available");
+ ota_decide_update("1.5.0", &rel, &d);
+ expect(strcmp(d.status, "uptodate") == 0, "decide uptodate");
+
+ ota_release_t bad;
+ expect(ota_parse_release("{\"tag_name\":\"v1.5.0\",\"assets\":[]}", &bad) == 0,
+ "missing ota asset");
+ expect(strcmp(bad.reason, "missing_ota_asset") == 0, "missing_ota_asset reason");
+
+ /* GitHub releases/latest shape: release-level name + fat uploader before browser_download_url. */
+ const char *github_json =
+ "{"
+ "\"tag_name\":\"v0.1.70\","
+ "\"name\":\"0.1.70\","
+ "\"assets\":["
+ "{"
+ "\"url\":\"https://api.github.com/repos/bryanthaboi/gen1recomp/releases/assets/502823880\","
+ "\"id\":502823880,"
+ "\"name\":\"gen1recomp-0.1.70-switch.zip\","
+ "\"label\":\"\","
+ "\"uploader\":{"
+ "\"login\":\"github-actions[bot]\","
+ "\"id\":41898282,"
+ "\"node_id\":\"MDM6Qm90NDE4OTgyODI=\","
+ "\"avatar_url\":\"https://avatars.githubusercontent.com/in/15368?v=4\","
+ "\"gravatar_id\":\"\","
+ "\"url\":\"https://api.github.com/users/github-actions%5Bbot%5D\","
+ "\"html_url\":\"https://github.com/apps/github-actions\","
+ "\"followers_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/followers\","
+ "\"following_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}\","
+ "\"gists_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}\","
+ "\"starred_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}\","
+ "\"subscriptions_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/subscriptions\","
+ "\"organizations_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/orgs\","
+ "\"repos_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/repos\","
+ "\"events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}\","
+ "\"received_events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/received_events\","
+ "\"type\":\"Bot\","
+ "\"user_view_type\":\"public\","
+ "\"site_admin\":false"
+ "},"
+ "\"content_type\":\"application/zip\","
+ "\"state\":\"uploaded\","
+ "\"size\":9000573,"
+ "\"browser_download_url\":\"https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.70/gen1recomp-0.1.70-switch.zip\""
+ "}"
+ "]"
+ "}";
+ ota_release_t gh;
+ expect(ota_parse_release(github_json, &gh) == 1, "parse github-shaped release");
+ expect(strcmp(gh.version, "0.1.70") == 0, "github release version");
+ expect(strcmp(gh.asset_name, "gen1recomp-0.1.70-switch.zip") == 0, "github release asset");
+ expect(strcmp(gh.download_url,
+ "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.70/gen1recomp-0.1.70-switch.zip") ==
+ 0,
+ "github release download url");
+ ota_decide_update("0.1.69", &gh, &d);
+ expect(strcmp(d.status, "available") == 0, "github release decide 0.1.69->0.1.70");
+
+ const char *sums = "abc123 gen1recomp-1.5.0-switch.zip\n";
+ ota_verify_t v;
+ ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "abc123", sums, &v);
+ expect(v.ok == 1, "sha ok");
+ ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "deadbeef", sums, &v);
+ expect(v.ok == 0 && strcmp(v.reason, "hash_mismatch") == 0, "sha mismatch");
+ ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "abc123", "", &v);
+ expect(v.ok == 0 && strcmp(v.reason, "sum_not_found") == 0, "sha missing sum rejected");
+
+ ota_apply_plan_t plan;
+ ota_plan_atomic_apply("switch/gen1recomp", "/tmp/x", &plan);
+ expect(strstr(plan.steps[0], "copy_to_part:") != NULL, "plan copy");
+ expect(strstr(plan.steps[1], "rename:") != NULL, "plan rename");
+ expect(strstr(plan.steps[2], "copy_to_part:launcher") != NULL, "plan launcher copy");
+ expect(strstr(plan.steps[3], "rename:") != NULL, "plan launcher rename");
+ expect(strstr(plan.steps[4], "env_set_next_load:") != NULL, "plan handoff");
+ expect(strstr(plan.preserve, "pokemon-love2d") != NULL, "preserve saves");
+ expect(strstr(plan.forbidden_delete, "delete:") != NULL, "forbid delete saves");
+ expect(strstr(plan.forbidden_direct, "write_direct:") != NULL, "forbid direct write");
+
+ ota_offline_t off;
+ ota_offline_events_t ev = {0};
+ ev.user_skip = 1;
+ ota_offline_policy(1.0, &ev, &off);
+ expect(strcmp(off.action, "play_installed") == 0, "skip plays installed");
+ ev.user_skip = 0;
+ ev.network_ok = 0;
+ ota_offline_policy(1.0, &ev, &off);
+ expect(strcmp(off.action, "play_installed") == 0, "offline plays installed");
+ ev.network_ok = 1;
+ ota_offline_policy(6.0, &ev, &off);
+ expect(strcmp(off.reason, "timeout") == 0, "timeout 6s");
+ ota_offline_policy(2.0, &ev, &off);
+ expect(strcmp(off.action, "keep_checking") == 0, "still checking");
+
+ expect(OTA_CHECK_TIMEOUT_SEC == 6, "timeout constant 6");
+
+ if (g_fail) {
+ fprintf(stderr, "%d failure(s)\n", g_fail);
+ return 1;
+ }
+ printf("all ota_protocol host tests passed\n");
+ return 0;
+}
diff --git a/ports/switch/ota-launcher/include/ota_fs.h b/ports/switch/ota-launcher/include/ota_fs.h
new file mode 100644
index 00000000..f94d98bd
--- /dev/null
+++ b/ports/switch/ota-launcher/include/ota_fs.h
@@ -0,0 +1,38 @@
+#ifndef GEN1_OTA_FS_H
+#define GEN1_OTA_FS_H
+
+#include "ota_protocol.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Read installed game version from sibling version.txt or NRO nacp-less stamp file.
+ * Expects switch/gen1recomp/version.txt containing X.Y.Z. Returns 0 on success. */
+int ota_fs_read_installed_version(const char *install_dir, char *out, size_t out_len);
+
+/* SHA-256 hex (lowercase) of file. Returns 0 on success. */
+int ota_fs_sha256_file(const char *path, char *out_hex, size_t out_len);
+
+/* Apply verified temp payload (extracted game NRO) using atomic plan. Returns 0 on success. */
+int ota_fs_atomic_replace_game(const char *install_dir, const char *verified_game_nro,
+ char *err, size_t err_len);
+
+/* Atomically replace any named NRO under install_dir (copy→.part→rename). */
+int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
+ const char *verified_nro, char *err, size_t err_len);
+
+/* Stage a new launcher + copy romfs bootstrap; chainload bootstrap_out next.
+ * The running launcher cannot replace its own NRO on sdmc/FAT. */
+int ota_fs_stage_launcher_bootstrap(const char *install_dir, const char *verified_launcher,
+ char *bootstrap_out, size_t bootstrap_out_len, char *err,
+ size_t err_len);
+
+/* Hand off to game NRO via envSetNextLoad (Switch) or no-op stub (host). */
+int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/ports/switch/ota-launcher/include/ota_net.h b/ports/switch/ota-launcher/include/ota_net.h
new file mode 100644
index 00000000..cd0c44c1
--- /dev/null
+++ b/ports/switch/ota-launcher/include/ota_net.h
@@ -0,0 +1,29 @@
+#ifndef GEN1_OTA_NET_H
+#define GEN1_OTA_NET_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* fraction is 0..1 when Content-Length is known, else -1 for indeterminate. */
+typedef void (*ota_net_progress_fn)(void *userdata, double fraction);
+
+/* Mount romfs (CA bundle) and init libcurl. Call before any HTTPS download. */
+int ota_net_init(void);
+void ota_net_shutdown(void);
+
+/* Download URL into memory buffer (caller frees *out). Returns 0 on success. */
+int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t *out_len,
+ char *err, size_t err_len);
+
+/* Download URL to a filesystem path. progress may be NULL. Returns 0 on success. */
+int ota_net_download_file(const char *url, const char *path, long timeout_ms, char *err,
+ size_t err_len, ota_net_progress_fn progress, void *progress_ud);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/ports/switch/ota-launcher/include/ota_protocol.h b/ports/switch/ota-launcher/include/ota_protocol.h
new file mode 100644
index 00000000..995efd7c
--- /dev/null
+++ b/ports/switch/ota-launcher/include/ota_protocol.h
@@ -0,0 +1,95 @@
+#ifndef GEN1_OTA_PROTOCOL_H
+#define GEN1_OTA_PROTOCOL_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define OTA_CHECK_TIMEOUT_SEC 6
+#define OTA_GAME_NRO_NAME "gen1recomp-game.nro"
+#define OTA_LAUNCHER_NRO_NAME "gen1recomp.nro"
+#define OTA_SAVE_DIR_NAME "pokemon-love2d"
+#define OTA_INSTALL_DIR "switch/gen1recomp"
+#define OTA_LAUNCHER_STAGED_SUFFIX ".staged"
+#define OTA_BOOTSTRAP_ROMFS "romfs:/ota-bootstrap.nro"
+#define OTA_BOOTSTRAP_SD_NAME "ota-bootstrap.nro"
+#define OTA_RELEASES_API \
+ "https://api.github.com/repos/bryanthaboi/gen1recomp/releases/latest"
+
+/* Mirrors src/update/SwitchOta.lua — keep semantics in lockstep. */
+
+int ota_compare_semver(const char *a, const char *b);
+int ota_is_ota_asset_name(const char *name);
+int ota_version_from_ota_asset(const char *name, char *out, size_t out_len);
+
+typedef struct {
+ int ok; /* 1 on success */
+ char reason[64];
+ char tag[64];
+ char version[32];
+ char asset_name[128];
+ char download_url[512];
+} ota_release_t;
+
+int ota_parse_release(const char *json_text, ota_release_t *out);
+
+typedef struct {
+ char status[32]; /* uptodate | available | error */
+ char reason[64];
+ char version[32];
+ char asset_name[128];
+ char download_url[512];
+} ota_decision_t;
+
+void ota_decide_update(const char *installed_version, const ota_release_t *release,
+ ota_decision_t *out);
+
+/* sums: newline-separated sha256sums.txt body */
+int ota_lookup_sum(const char *sums_text, const char *asset_name, char *out_hex,
+ size_t out_len);
+
+typedef struct {
+ int ok;
+ char reason[64];
+} ota_verify_t;
+
+void ota_verify_sha256(const char *asset_name, const char *actual_hex,
+ const char *sums_text, ota_verify_t *out);
+
+typedef struct {
+ char steps[5][520]; /* human-readable ops */
+ char preserve[256];
+ char forbidden_delete[256];
+ char forbidden_direct[256];
+ char part_path[256];
+ char game_nro[240];
+ char launcher_nro[240];
+ char launcher_part[256];
+ char next_load[240];
+} ota_apply_plan_t;
+
+void ota_plan_atomic_apply(const char *install_dir, const char *verified_temp,
+ ota_apply_plan_t *out);
+
+typedef struct {
+ char action[32]; /* play_installed | keep_checking */
+ char reason[64];
+ char message[128];
+} ota_offline_t;
+
+typedef struct {
+ int user_skip;
+ int network_ok; /* 0 = offline/fail, 1 = ok, -1 = unknown */
+ int api_error;
+} ota_offline_events_t;
+
+void ota_offline_policy(double elapsed_sec, const ota_offline_events_t *events,
+ ota_offline_t *out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GEN1_OTA_PROTOCOL_H */
diff --git a/ports/switch/ota-launcher/include/ota_ui.h b/ports/switch/ota-launcher/include/ota_ui.h
new file mode 100644
index 00000000..0ad19e6f
--- /dev/null
+++ b/ports/switch/ota-launcher/include/ota_ui.h
@@ -0,0 +1,37 @@
+#ifndef GEN1_OTA_UI_H
+#define GEN1_OTA_UI_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Framebuffer UI matching the in-game launcher look (black + RGB rail +
+ * flat semantic buttons). Silent by default — only call when user-facing. */
+
+/* Returns 1 = update, 0 = skip / play installed. */
+int ota_ui_prompt_update(const char *installed, const char *latest);
+
+/* Status screen (download / install). Redraws each call. */
+void ota_ui_show_status(const char *title, const char *detail);
+
+/* Progress 0..1; detail optional. */
+void ota_ui_show_progress(const char *title, const char *detail, float progress01);
+
+/* Error / info + wait for B. */
+void ota_ui_alert(const char *title, const char *line1, const char *line2);
+
+/* User-friendly error with optional technical detail and installed-version footer. */
+void ota_ui_alert_error(const char *title, const char *friendly, const char *technical,
+ const char *installed_version);
+
+/* Missing game install screen; wait for +. */
+void ota_ui_missing_game(void);
+
+/* Tear down framebuffer if active. Safe to call when UI never opened. */
+void ota_ui_shutdown(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/ports/switch/ota-launcher/include/ota_unzip.h b/ports/switch/ota-launcher/include/ota_unzip.h
new file mode 100644
index 00000000..83e9c9ca
--- /dev/null
+++ b/ports/switch/ota-launcher/include/ota_unzip.h
@@ -0,0 +1,20 @@
+#ifndef GEN1_OTA_UNZIP_H
+#define GEN1_OTA_UNZIP_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Extract member_name from zip_path into dest_path.
+ * Tries exact member_name, then common prefixes (switch/gen1recomp/).
+ * Returns 0 on success. */
+int ota_unzip_extract_file(const char *zip_path, const char *member_name, const char *dest_path,
+ char *err, size_t err_len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/ports/switch/ota-launcher/romfs/.gitkeep b/ports/switch/ota-launcher/romfs/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/ports/switch/ota-launcher/src/main.c b/ports/switch/ota-launcher/src/main.c
new file mode 100644
index 00000000..f187e421
--- /dev/null
+++ b/ports/switch/ota-launcher/src/main.c
@@ -0,0 +1,281 @@
+/*
+ * gen1recomp Switch OTA launcher
+ *
+ * Quiet by default: checks GitHub Releases with no UI. Only shows the
+ * branded launcher-style screen when an update is available. Downloads the
+ * same SD zip (gen1recomp-*-switch.zip), verifies SHA-256, replaces game +
+ * launcher NROs, then envSetNextLoad. Never touches pokemon-love2d/.
+ * LÖVE self-updater stays off on NX.
+ */
+
+#include "ota_fs.h"
+#include "ota_net.h"
+#include "ota_protocol.h"
+#include "ota_ui.h"
+#include "ota_unzip.h"
+
+#include
+#include
+#include
+
+#if defined(__SWITCH__)
+#include
+#include
+#include
+#else
+#include
+#include
+#endif
+
+#define SD_INSTALL_DIR "sdmc:/switch/gen1recomp"
+#define CHECK_TIMEOUT_MS (OTA_CHECK_TIMEOUT_SEC * 1000L)
+#define GAME_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_GAME_NRO_NAME
+#define LAUNCHER_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_LAUNCHER_NRO_NAME
+
+typedef struct {
+ const char *detail;
+ float base;
+ float span;
+} download_progress_ctx_t;
+
+static void on_download_progress(void *userdata, double fraction) {
+ download_progress_ctx_t *ctx = (download_progress_ctx_t *)userdata;
+ if (!ctx) return;
+ float p = ctx->base;
+ if (fraction >= 0.0) p += ctx->span * (float)fraction;
+ else p += ctx->span * 0.1f;
+ if (p > ctx->base + ctx->span) p = ctx->base + ctx->span;
+ ota_ui_show_progress("Step 1/3: Downloading...", ctx->detail, p);
+}
+
+static void show_update_error(const char *title, const char *friendly, const char *technical,
+ const char *installed) {
+ ota_ui_alert_error(title, friendly, technical, installed);
+}
+
+static int run_update_flow(const char *install_dir) {
+ char installed[64] = "0.0.0";
+ (void)ota_fs_read_installed_version(install_dir, installed, sizeof(installed));
+
+ char *json = NULL;
+ size_t json_len = 0;
+ char err[256];
+ err[0] = '\0';
+
+ if (ota_net_download_buffer(OTA_RELEASES_API, CHECK_TIMEOUT_MS, &json, &json_len, err,
+ sizeof(err)) != 0) {
+ free(json);
+ return 0; /* offline / timeout — play installed, no UI */
+ }
+
+ ota_release_t rel;
+ if (!ota_parse_release(json, &rel)) {
+ free(json);
+ return 0;
+ }
+ free(json);
+
+ ota_decision_t dec;
+ ota_decide_update(installed, &rel, &dec);
+ if (strcmp(dec.status, "uptodate") == 0 || strcmp(dec.status, "error") == 0) {
+ return 0;
+ }
+
+ if (!ota_ui_prompt_update(installed, dec.version)) {
+ return 0;
+ }
+
+ char updates_dir[192];
+ char zip_path[256];
+ char sums_path[256];
+ snprintf(updates_dir, sizeof(updates_dir), "%s/updates", install_dir);
+ snprintf(zip_path, sizeof(zip_path), "%s/updates/%s", install_dir, dec.asset_name);
+ snprintf(sums_path, sizeof(sums_path), "%s/updates/sha256sums.txt", install_dir);
+
+#if defined(__SWITCH__)
+ mkdir(updates_dir, 0755);
+#endif
+
+ download_progress_ctx_t dl_ctx = {"This may take a minute.", 0.f, 0.5f};
+ ota_ui_show_progress("Step 1/3: Downloading...", dl_ctx.detail, dl_ctx.base);
+ if (ota_net_download_file(dec.download_url, zip_path, 180000L, err, sizeof(err),
+ on_download_progress, &dl_ctx) != 0) {
+ show_update_error("Download failed",
+ "Could not download the update. Check Wi-Fi and try again.", err, installed);
+ return 0;
+ }
+
+ ota_ui_show_progress("Step 2/3: Verifying...", "Checking file integrity.", 0.55f);
+ char sums_url[512];
+ snprintf(sums_url, sizeof(sums_url),
+ "https://github.com/bryanthaboi/gen1recomp/releases/download/%s/sha256sums.txt",
+ rel.tag);
+ if (ota_net_download_file(sums_url, sums_path, CHECK_TIMEOUT_MS, err, sizeof(err), NULL,
+ NULL) != 0) {
+ remove(zip_path);
+ show_update_error("Could not verify", "Downloaded file could not be checked.", err, installed);
+ return 0;
+ }
+
+ FILE *sf = fopen(sums_path, "rb");
+ if (!sf) {
+ remove(zip_path);
+ show_update_error("Could not verify", "Could not read checksum file.", "fopen sums failed",
+ installed);
+ return 0;
+ }
+ fseek(sf, 0, SEEK_END);
+ long slen = ftell(sf);
+ fseek(sf, 0, SEEK_SET);
+ char *sums = (char *)malloc((size_t)slen + 1);
+ if (!sums) {
+ fclose(sf);
+ remove(zip_path);
+ show_update_error("Could not verify", "Not enough memory to verify update.", "malloc failed",
+ installed);
+ return 0;
+ }
+ fread(sums, 1, (size_t)slen, sf);
+ sums[slen] = '\0';
+ fclose(sf);
+
+ ota_ui_show_progress("Step 2/3: Verifying...", "Computing checksum...", 0.65f);
+ char hex[96];
+ if (ota_fs_sha256_file(zip_path, hex, sizeof(hex)) != 0) {
+ free(sums);
+ remove(zip_path);
+ show_update_error("Could not verify", "Could not read downloaded update file.",
+ "sha256 file read failed", installed);
+ return 0;
+ }
+ ota_verify_t ver;
+ ota_verify_sha256(dec.asset_name, hex, sums, &ver);
+ free(sums);
+ if (!ver.ok) {
+ remove(zip_path);
+ show_update_error("Update check failed",
+ "Downloaded file did not match expected checksum.", ver.reason, installed);
+ return 0;
+ }
+
+ char extracted[256];
+ char extracted_launcher[256];
+ snprintf(extracted, sizeof(extracted), "%s/updates/gen1recomp-game.nro.verified", install_dir);
+ snprintf(extracted_launcher, sizeof(extracted_launcher), "%s/updates/gen1recomp.nro.verified",
+ install_dir);
+
+#if defined(__SWITCH__)
+ ota_ui_show_progress("Step 3/3: Installing...", "Keeping your saves safe.", 0.8f);
+ if (ota_unzip_extract_file(zip_path, GAME_MEMBER_IN_ZIP, extracted, err, sizeof(err)) != 0) {
+ if (ota_unzip_extract_file(zip_path, OTA_GAME_NRO_NAME, extracted, err, sizeof(err)) != 0) {
+ remove(zip_path);
+ show_update_error("Could not extract",
+ "Update zip is missing game files or is corrupted.", err, installed);
+ return 0;
+ }
+ }
+
+ if (ota_fs_atomic_replace_game(install_dir, extracted, err, sizeof(err)) != 0) {
+ remove(extracted);
+ remove(zip_path);
+ show_update_error("Could not install",
+ "Could not replace game on microSD. Free up space and try again.", err,
+ installed);
+ return 0;
+ }
+ remove(extracted);
+
+ int have_launcher = 0;
+ if (ota_unzip_extract_file(zip_path, LAUNCHER_MEMBER_IN_ZIP, extracted_launcher, err,
+ sizeof(err)) == 0 ||
+ ota_unzip_extract_file(zip_path, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err,
+ sizeof(err)) == 0) {
+ have_launcher = 1;
+ }
+ if (!have_launcher) {
+ remove(zip_path);
+ show_update_error("Could not extract",
+ "Update zip is missing launcher files.", err, installed);
+ return 0;
+ }
+
+ char bootstrap_path[256];
+ bootstrap_path[0] = '\0';
+ if (ota_fs_stage_launcher_bootstrap(install_dir, extracted_launcher, bootstrap_path,
+ sizeof(bootstrap_path), err, sizeof(err)) != 0) {
+ remove(extracted_launcher);
+ remove(zip_path);
+ show_update_error("Could not install",
+ "Game updated but launcher could not be staged. Reinstall from the SD zip.",
+ err, installed);
+ return 0;
+ }
+ remove(extracted_launcher);
+
+ char vpath[192];
+ snprintf(vpath, sizeof(vpath), "%s/version.txt", install_dir);
+ FILE *vf = fopen(vpath, "wb");
+ if (vf) {
+ fprintf(vf, "%s\n", dec.version);
+ fclose(vf);
+ }
+
+ ota_ui_show_progress("Ready", "Finishing update...", 1.0f);
+ svcSleepThread(600000000ULL);
+ remove(zip_path);
+ ota_ui_shutdown();
+
+ if (ota_fs_handoff_to_game(bootstrap_path, err, sizeof(err)) != 0) {
+ show_update_error("Could not install", "Launcher bootstrap failed.", err, installed);
+ return 0;
+ }
+ return 2; /* chainload bootstrap; main must not hand off to game */
+#else
+ (void)extracted;
+ (void)extracted_launcher;
+#endif
+ remove(zip_path);
+ ota_ui_shutdown();
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ (void)argc;
+ (void)argv;
+
+ const char *install = SD_INSTALL_DIR;
+ int update_rc = 0;
+
+#if defined(__SWITCH__)
+ socketInitializeDefault();
+ padConfigureInput(1, HidNpadStyleSet_NpadStandard);
+ if (ota_net_init() == 0) {
+ update_rc = run_update_flow(install);
+ ota_net_shutdown();
+ }
+#else
+ update_rc = run_update_flow(install);
+#endif
+
+ if (update_rc == 2) {
+#if defined(__SWITCH__)
+ socketExit();
+#endif
+ return 0;
+ }
+
+ char game[192];
+ snprintf(game, sizeof(game), "%s/%s", install, OTA_GAME_NRO_NAME);
+ char err[128];
+ err[0] = '\0';
+ if (ota_fs_handoff_to_game(game, err, sizeof(err)) != 0) {
+ ota_ui_missing_game();
+ }
+
+ ota_ui_shutdown();
+
+#if defined(__SWITCH__)
+ socketExit();
+#endif
+ return 0;
+}
diff --git a/ports/switch/ota-launcher/src/ota_fs.c b/ports/switch/ota-launcher/src/ota_fs.c
new file mode 100644
index 00000000..5adca96b
--- /dev/null
+++ b/ports/switch/ota-launcher/src/ota_fs.c
@@ -0,0 +1,177 @@
+#include "ota_fs.h"
+
+#include
+#include
+#include
+
+#if defined(__SWITCH__)
+#include
+#include
+#include
+#include
+#else
+#include
+#endif
+
+int ota_fs_read_installed_version(const char *install_dir, char *out, size_t out_len) {
+ if (!out || out_len == 0) return -1;
+ out[0] = '\0';
+ char path[256];
+ snprintf(path, sizeof(path), "%s/version.txt", install_dir ? install_dir : OTA_INSTALL_DIR);
+ FILE *fp = fopen(path, "rb");
+ if (!fp) return -1;
+ if (!fgets(out, (int)out_len, fp)) {
+ fclose(fp);
+ return -1;
+ }
+ fclose(fp);
+ /* trim */
+ size_t n = strlen(out);
+ while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ')) {
+ out[--n] = '\0';
+ }
+ return n > 0 ? 0 : -1;
+}
+
+int ota_fs_sha256_file(const char *path, char *out_hex, size_t out_len) {
+ if (!path || !out_hex || out_len < 65) return -1;
+ out_hex[0] = '\0';
+ FILE *fp = fopen(path, "rb");
+ if (!fp) return -1;
+
+#if defined(__SWITCH__)
+ mbedtls_sha256_context ctx;
+ mbedtls_sha256_init(&ctx);
+ mbedtls_sha256_starts(&ctx, 0);
+ unsigned char buf[8192];
+ size_t n;
+ while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
+ mbedtls_sha256_update(&ctx, buf, n);
+ }
+ fclose(fp);
+ unsigned char digest[32];
+ mbedtls_sha256_finish(&ctx, digest);
+ mbedtls_sha256_free(&ctx);
+ for (int i = 0; i < 32; i++) snprintf(out_hex + i * 2, out_len - (size_t)(i * 2), "%02x", digest[i]);
+ return 0;
+#else
+ /* Host: shell out to shasum/sha256sum for the host test path if needed.
+ * Protocol host tests do not call this; return not-implemented. */
+ fclose(fp);
+ (void)out_len;
+ return -1;
+#endif
+}
+
+static int copy_file(const char *from, const char *to) {
+ FILE *in = fopen(from, "rb");
+ if (!in) return -1;
+ FILE *out = fopen(to, "wb");
+ if (!out) {
+ fclose(in);
+ return -1;
+ }
+ char buf[8192];
+ size_t n;
+ while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
+ if (fwrite(buf, 1, n, out) != n) {
+ fclose(in);
+ fclose(out);
+ return -1;
+ }
+ }
+ fclose(in);
+ fclose(out);
+ return 0;
+}
+
+int ota_fs_atomic_replace_game(const char *install_dir, const char *verified_game_nro,
+ char *err, size_t err_len) {
+ return ota_fs_atomic_replace_nro(install_dir, OTA_GAME_NRO_NAME, verified_game_nro, err,
+ err_len);
+}
+
+int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
+ const char *verified_nro, char *err, size_t err_len) {
+ if (!nro_name || !*nro_name || !verified_nro || !*verified_nro) {
+ if (err && err_len) snprintf(err, err_len, "missing nro paths");
+ return -1;
+ }
+ char dest[240];
+ char part[256];
+ snprintf(dest, sizeof(dest), "%s/%s", install_dir ? install_dir : OTA_INSTALL_DIR, nro_name);
+ snprintf(part, sizeof(part), "%s.part", dest);
+ if (copy_file(verified_nro, part) != 0) {
+ if (err && err_len) snprintf(err, err_len, "copy to .part failed (%s)", nro_name);
+ return -1;
+ }
+ /* sdmc/FAT (Switch) and Windows do not replace an existing dest on rename. */
+ remove(dest);
+ if (rename(part, dest) != 0) {
+ if (err && err_len) snprintf(err, err_len, "rename .part -> %s failed", nro_name);
+ remove(part);
+ return -1;
+ }
+ return 0;
+}
+
+int ota_fs_stage_launcher_bootstrap(const char *install_dir, const char *verified_launcher,
+ char *bootstrap_out, size_t bootstrap_out_len, char *err,
+ size_t err_len) {
+ if (!verified_launcher || !*verified_launcher) {
+ if (err && err_len) snprintf(err, err_len, "missing staged launcher path");
+ return -1;
+ }
+ if (!bootstrap_out || bootstrap_out_len == 0) {
+ if (err && err_len) snprintf(err, err_len, "missing bootstrap output buffer");
+ return -1;
+ }
+ bootstrap_out[0] = '\0';
+
+ const char *base = install_dir ? install_dir : OTA_INSTALL_DIR;
+ char staged[256];
+ char bootstrap[256];
+ char updates[192];
+ snprintf(staged, sizeof(staged), "%s/%s%s", base, OTA_LAUNCHER_NRO_NAME,
+ OTA_LAUNCHER_STAGED_SUFFIX);
+ snprintf(updates, sizeof(updates), "%s/updates", base);
+ snprintf(bootstrap, sizeof(bootstrap), "%s/updates/%s", base, OTA_BOOTSTRAP_SD_NAME);
+
+ remove(staged);
+ if (copy_file(verified_launcher, staged) != 0) {
+ if (err && err_len) snprintf(err, err_len, "stage launcher failed");
+ return -1;
+ }
+
+#if defined(__SWITCH__)
+ mkdir(updates, 0755);
+#endif
+ if (copy_file(OTA_BOOTSTRAP_ROMFS, bootstrap) != 0) {
+ remove(staged);
+ if (err && err_len) snprintf(err, err_len, "extract bootstrap failed");
+ return -1;
+ }
+
+ snprintf(bootstrap_out, bootstrap_out_len, "%s", bootstrap);
+ return 0;
+}
+
+int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len) {
+ if (!game_nro_path || !*game_nro_path) {
+ if (err && err_len) snprintf(err, err_len, "missing game path");
+ return -1;
+ }
+#if defined(__SWITCH__)
+ /* argv for next load: empty args string is fine */
+ Result rc = envSetNextLoad(game_nro_path, game_nro_path);
+ if (R_FAILED(rc)) {
+ if (err && err_len) snprintf(err, err_len, "envSetNextLoad failed: 0x%x", rc);
+ return -1;
+ }
+ return 0;
+#else
+ if (err && err_len)
+ snprintf(err, err_len, "handoff stub (host): would envSetNextLoad %s", game_nro_path);
+ return 0; /* host stub succeeds for flow tests */
+#endif
+}
diff --git a/ports/switch/ota-launcher/src/ota_net.c b/ports/switch/ota-launcher/src/ota_net.c
new file mode 100644
index 00000000..7b897b04
--- /dev/null
+++ b/ports/switch/ota-launcher/src/ota_net.c
@@ -0,0 +1,185 @@
+#include "ota_net.h"
+
+#include
+#include
+#include
+
+#if defined(__SWITCH__)
+#include
+#include
+#endif
+
+#if defined(__SWITCH__)
+#define OTA_CA_BUNDLE "romfs:/cacert.pem"
+
+static int g_net_ready = 0;
+
+static int ota_ca_bundle_ready(void) {
+ FILE *f = fopen(OTA_CA_BUNDLE, "rb");
+ if (!f) return 0;
+ fclose(f);
+ return 1;
+}
+
+int ota_net_init(void) {
+ if (g_net_ready) return 0;
+ if (R_FAILED(romfsInit())) return -1;
+ if (!ota_ca_bundle_ready()) return -1;
+ if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return -1;
+ g_net_ready = 1;
+ return 0;
+}
+
+void ota_net_shutdown(void) {
+ if (!g_net_ready) return;
+ curl_global_cleanup();
+ romfsExit();
+ g_net_ready = 0;
+}
+
+static void ota_net_configure_tls(CURL *curl) {
+ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
+ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
+ curl_easy_setopt(curl, CURLOPT_CAINFO, OTA_CA_BUNDLE);
+}
+
+struct mem_buf {
+ char *data;
+ size_t len;
+};
+
+struct file_progress {
+ ota_net_progress_fn fn;
+ void *userdata;
+};
+
+static size_t write_mem(char *ptr, size_t size, size_t nmemb, void *userdata) {
+ struct mem_buf *m = (struct mem_buf *)userdata;
+ size_t n = size * nmemb;
+ char *p = (char *)realloc(m->data, m->len + n + 1);
+ if (!p) return 0;
+ m->data = p;
+ memcpy(m->data + m->len, ptr, n);
+ m->len += n;
+ m->data[m->len] = '\0';
+ return n;
+}
+
+static size_t write_file(char *ptr, size_t size, size_t nmemb, void *userdata) {
+ return fwrite(ptr, size, nmemb, (FILE *)userdata);
+}
+
+static int xfer_progress(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
+ curl_off_t ulnow) {
+ (void)ultotal;
+ (void)ulnow;
+ struct file_progress *fp = (struct file_progress *)clientp;
+ if (!fp || !fp->fn) return 0;
+ if (dltotal > 0) {
+ double frac = (double)dlnow / (double)dltotal;
+ if (frac < 0.0) frac = 0.0;
+ if (frac > 1.0) frac = 1.0;
+ fp->fn(fp->userdata, frac);
+ } else if (dlnow > 0) {
+ fp->fn(fp->userdata, -1.0);
+ }
+ return 0;
+}
+#else
+
+int ota_net_init(void) { return 0; }
+void ota_net_shutdown(void) {}
+
+#endif
+
+int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t *out_len,
+ char *err, size_t err_len) {
+ if (out) *out = NULL;
+ if (out_len) *out_len = 0;
+ if (!url || !out) {
+ if (err && err_len) snprintf(err, err_len, "bad args");
+ return -1;
+ }
+#if defined(__SWITCH__)
+ CURL *curl = curl_easy_init();
+ if (!curl) {
+ if (err && err_len) snprintf(err, err_len, "curl_easy_init failed");
+ return -1;
+ }
+ struct mem_buf mem = {0};
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, "gen1recomp-switch-ota");
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
+ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_mem);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
+ ota_net_configure_tls(curl);
+ CURLcode rc = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+ if (rc != CURLE_OK) {
+ free(mem.data);
+ if (err && err_len) snprintf(err, err_len, "%s", curl_easy_strerror(rc));
+ return -1;
+ }
+ *out = mem.data;
+ if (out_len) *out_len = mem.len;
+ return 0;
+#else
+ (void)timeout_ms;
+ if (err && err_len)
+ snprintf(err, err_len, "ota_net_download_buffer only available on __SWITCH__");
+ return -1;
+#endif
+}
+
+int ota_net_download_file(const char *url, const char *path, long timeout_ms, char *err,
+ size_t err_len, ota_net_progress_fn progress, void *progress_ud) {
+ if (!url || !path) {
+ if (err && err_len) snprintf(err, err_len, "bad args");
+ return -1;
+ }
+#if defined(__SWITCH__)
+ FILE *fp = fopen(path, "wb");
+ if (!fp) {
+ if (err && err_len) snprintf(err, err_len, "fopen failed: %s", path);
+ return -1;
+ }
+ CURL *curl = curl_easy_init();
+ if (!curl) {
+ fclose(fp);
+ if (err && err_len) snprintf(err, err_len, "curl_easy_init failed");
+ return -1;
+ }
+ struct file_progress fp_cb = {progress, progress_ud};
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, "gen1recomp-switch-ota");
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
+ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
+ ota_net_configure_tls(curl);
+ if (progress) {
+ curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
+ curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xfer_progress);
+ curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &fp_cb);
+ }
+ CURLcode rc = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+ fclose(fp);
+ if (rc != CURLE_OK) {
+ if (err && err_len) snprintf(err, err_len, "%s", curl_easy_strerror(rc));
+ return -1;
+ }
+ if (progress) progress(progress_ud, 1.0);
+ return 0;
+#else
+ (void)timeout_ms;
+ (void)progress;
+ (void)progress_ud;
+ if (err && err_len)
+ snprintf(err, err_len, "ota_net_download_file only available on __SWITCH__");
+ return -1;
+#endif
+}
diff --git a/ports/switch/ota-launcher/src/ota_protocol.c b/ports/switch/ota-launcher/src/ota_protocol.c
new file mode 100644
index 00000000..ba41b121
--- /dev/null
+++ b/ports/switch/ota-launcher/src/ota_protocol.c
@@ -0,0 +1,329 @@
+#include "ota_protocol.h"
+
+#include
+#include
+#include
+#include
+
+typedef struct {
+ int major;
+ int minor;
+ int patch;
+ int ok;
+} semver_t;
+
+static void set_reason(char *buf, size_t n, const char *r) {
+ if (!buf || n == 0) return;
+ snprintf(buf, n, "%s", r ? r : "");
+}
+
+static int parse_semver(const char *s, semver_t *out) {
+ memset(out, 0, sizeof(*out));
+ if (!s || !*s) return 0;
+ if (s[0] == 'v' || s[0] == 'V') s++;
+ int maj = 0, min = 0, pat = 0;
+ char trail = 0;
+ if (sscanf(s, "%d.%d.%d%c", &maj, &min, &pat, &trail) != 3) return 0;
+ if (maj < 0 || min < 0 || pat < 0) return 0;
+ out->major = maj;
+ out->minor = min;
+ out->patch = pat;
+ out->ok = 1;
+ return 1;
+}
+
+int ota_compare_semver(const char *a, const char *b) {
+ semver_t pa, pb;
+ int oa = parse_semver(a, &pa);
+ int ob = parse_semver(b, &pb);
+ if (!oa && !ob) return 0;
+ if (!oa) return -1;
+ if (!ob) return 1;
+ if (pa.major != pb.major) return pa.major < pb.major ? -1 : 1;
+ if (pa.minor != pb.minor) return pa.minor < pb.minor ? -1 : 1;
+ if (pa.patch != pb.patch) return pa.patch < pb.patch ? -1 : 1;
+ return 0;
+}
+
+int ota_is_ota_asset_name(const char *name) {
+ int maj = 0, min = 0, pat = 0;
+ if (!name) return 0;
+ /* Unified SD/OTA asset: gen1recomp-X.Y.Z-switch.zip */
+ size_t len = strlen(name);
+ const char *suffix = "-switch.zip";
+ size_t slen = strlen(suffix);
+ if (len <= slen) return 0;
+ if (strcmp(name + len - slen, suffix) != 0) return 0;
+ if (strncmp(name, "gen1recomp-", 11) != 0) return 0;
+ if (sscanf(name + 11, "%d.%d.%d", &maj, &min, &pat) != 3) return 0;
+ char rebuilt[128];
+ snprintf(rebuilt, sizeof(rebuilt), "gen1recomp-%d.%d.%d-switch.zip", maj, min, pat);
+ return strcmp(name, rebuilt) == 0;
+}
+
+int ota_version_from_ota_asset(const char *name, char *out, size_t out_len) {
+ int maj = 0, min = 0, pat = 0;
+ if (!out || out_len == 0) return 0;
+ out[0] = '\0';
+ if (!ota_is_ota_asset_name(name)) return 0;
+ if (sscanf(name + 11, "%d.%d.%d", &maj, &min, &pat) != 3) return 0;
+ snprintf(out, out_len, "%d.%d.%d", maj, min, pat);
+ return 1;
+}
+
+static const char *find_json_string(const char *json, const char *key, char *out, size_t out_len) {
+ char pattern[128];
+ snprintf(pattern, sizeof(pattern), "\"%s\"", key);
+ const char *p = strstr(json, pattern);
+ if (!p) return NULL;
+ p = strchr(p + strlen(pattern), ':');
+ if (!p) return NULL;
+ p++;
+ while (*p && isspace((unsigned char)*p)) p++;
+ if (*p != '"') return NULL;
+ p++;
+ size_t i = 0;
+ while (*p && *p != '"' && i + 1 < out_len) {
+ if (*p == '\\' && p[1]) {
+ p++;
+ out[i++] = *p++;
+ continue;
+ }
+ out[i++] = *p++;
+ }
+ out[i] = '\0';
+ return out;
+}
+
+/* Opening { of the JSON object that contains pos (walk backward). */
+static const char *find_json_object_start(const char *pos, const char *json_start) {
+ if (!pos || !json_start || pos < json_start) return NULL;
+ int depth = 0;
+ const char *p = pos;
+ while (p >= json_start) {
+ if (*p == '}') depth++;
+ else if (*p == '{') {
+ if (depth == 0) return p;
+ depth--;
+ }
+ p--;
+ }
+ return NULL;
+}
+
+/* Pointer just past the closing } of the object that starts at object_start. */
+static const char *find_json_object_end(const char *object_start) {
+ if (!object_start || *object_start != '{') return NULL;
+ int depth = 1;
+ const char *p = object_start + 1;
+ while (*p) {
+ if (*p == '{') depth++;
+ else if (*p == '}') {
+ depth--;
+ if (depth == 0) return p + 1;
+ }
+ p++;
+ }
+ return NULL;
+}
+
+int ota_parse_release(const char *json_text, ota_release_t *out) {
+ memset(out, 0, sizeof(*out));
+ if (!json_text || !*json_text) {
+ set_reason(out->reason, sizeof(out->reason), "empty_json");
+ return 0;
+ }
+ if (!find_json_string(json_text, "tag_name", out->tag, sizeof(out->tag))) {
+ set_reason(out->reason, sizeof(out->reason), "missing_tag");
+ return 0;
+ }
+ semver_t sv;
+ if (!parse_semver(out->tag, &sv)) {
+ set_reason(out->reason, sizeof(out->reason), "bad_tag");
+ return 0;
+ }
+ snprintf(out->version, sizeof(out->version), "%d.%d.%d", sv.major, sv.minor, sv.patch);
+
+ /* Scan for OTA asset name then browser_download_url inside the same asset object. */
+ const char *cursor = json_text;
+ while ((cursor = strstr(cursor, "\"name\"")) != NULL) {
+ char name[128];
+ if (!find_json_string(cursor, "name", name, sizeof(name))) {
+ cursor += 6;
+ continue;
+ }
+ if (!ota_is_ota_asset_name(name)) {
+ cursor += 6;
+ continue;
+ }
+ const char *asset_start = find_json_object_start(cursor, json_text);
+ const char *asset_end =
+ asset_start ? find_json_object_end(asset_start) : NULL;
+ if (!asset_start || !asset_end || asset_end <= cursor) {
+ cursor += 6;
+ continue;
+ }
+ char url[512];
+ const char *u = NULL;
+ const char *scan = cursor;
+ while (scan < asset_end &&
+ (scan = strstr(scan, "\"browser_download_url\"")) != NULL && scan < asset_end) {
+ if (find_json_string(scan, "browser_download_url", url, sizeof(url))) {
+ u = url;
+ break;
+ }
+ scan += 21;
+ }
+ if (!u || !*u) {
+ cursor += 6;
+ continue;
+ }
+ snprintf(out->asset_name, sizeof(out->asset_name), "%s", name);
+ snprintf(out->download_url, sizeof(out->download_url), "%s", u);
+ out->ok = 1;
+ return 1;
+ }
+ set_reason(out->reason, sizeof(out->reason), "missing_ota_asset");
+ return 0;
+}
+
+void ota_decide_update(const char *installed_version, const ota_release_t *release,
+ ota_decision_t *out) {
+ memset(out, 0, sizeof(*out));
+ semver_t inst;
+ if (!parse_semver(installed_version, &inst)) {
+ snprintf(out->status, sizeof(out->status), "error");
+ set_reason(out->reason, sizeof(out->reason), "bad_installed_version");
+ return;
+ }
+ if (!release || !release->ok) {
+ snprintf(out->status, sizeof(out->status), "error");
+ set_reason(out->reason, sizeof(out->reason), "bad_release");
+ return;
+ }
+ if (ota_compare_semver(release->version, installed_version) <= 0) {
+ snprintf(out->status, sizeof(out->status), "uptodate");
+ snprintf(out->version, sizeof(out->version), "%s", installed_version);
+ return;
+ }
+ snprintf(out->status, sizeof(out->status), "available");
+ snprintf(out->version, sizeof(out->version), "%s", release->version);
+ snprintf(out->asset_name, sizeof(out->asset_name), "%s", release->asset_name);
+ snprintf(out->download_url, sizeof(out->download_url), "%s", release->download_url);
+}
+
+int ota_lookup_sum(const char *sums_text, const char *asset_name, char *out_hex, size_t out_len) {
+ if (out_hex && out_len) out_hex[0] = '\0';
+ if (!sums_text || !asset_name || !out_hex || out_len < 65) return 0;
+ const char *p = sums_text;
+ while (*p) {
+ char hex[96];
+ char name[256];
+ int n = 0;
+ if (sscanf(p, "%95s %255s%n", hex, name, &n) >= 2 && n > 0) {
+ const char *nm = name;
+ if (nm[0] == '*') nm++;
+ if (strncmp(nm, "./", 2) == 0) nm += 2;
+ if (strcmp(nm, asset_name) == 0) {
+ /* normalize hex to lowercase */
+ size_t i;
+ for (i = 0; hex[i] && i + 1 < out_len; i++)
+ out_hex[i] = (char)tolower((unsigned char)hex[i]);
+ out_hex[i] = '\0';
+ return 1;
+ }
+ p += n;
+ while (*p && *p != '\n') p++;
+ if (*p == '\n') p++;
+ continue;
+ }
+ while (*p && *p != '\n') p++;
+ if (*p == '\n') p++;
+ }
+ return 0;
+}
+
+void ota_verify_sha256(const char *asset_name, const char *actual_hex, const char *sums_text,
+ ota_verify_t *out) {
+ memset(out, 0, sizeof(*out));
+ if (!asset_name || !*asset_name) {
+ set_reason(out->reason, sizeof(out->reason), "bad_asset_name");
+ return;
+ }
+ if (!sums_text) {
+ set_reason(out->reason, sizeof(out->reason), "missing_sums");
+ return;
+ }
+ char expected[96];
+ if (!ota_lookup_sum(sums_text, asset_name, expected, sizeof(expected))) {
+ set_reason(out->reason, sizeof(out->reason), "sum_not_found");
+ return;
+ }
+ if (!actual_hex || !*actual_hex) {
+ set_reason(out->reason, sizeof(out->reason), "missing_actual_hash");
+ return;
+ }
+ char actual[96];
+ size_t i;
+ for (i = 0; actual_hex[i] && i + 1 < sizeof(actual); i++)
+ actual[i] = (char)tolower((unsigned char)actual_hex[i]);
+ actual[i] = '\0';
+ if (strcmp(actual, expected) != 0) {
+ set_reason(out->reason, sizeof(out->reason), "hash_mismatch");
+ return;
+ }
+ out->ok = 1;
+}
+
+void ota_plan_atomic_apply(const char *install_dir, const char *verified_temp,
+ ota_apply_plan_t *out) {
+ memset(out, 0, sizeof(*out));
+ if (!install_dir || !*install_dir) install_dir = OTA_INSTALL_DIR;
+ if (!verified_temp) verified_temp = "";
+ snprintf(out->game_nro, sizeof(out->game_nro), "%s/%s", install_dir, OTA_GAME_NRO_NAME);
+ snprintf(out->part_path, sizeof(out->part_path), "%s.part", out->game_nro);
+ snprintf(out->launcher_nro, sizeof(out->launcher_nro), "%s/%s", install_dir,
+ OTA_LAUNCHER_NRO_NAME);
+ snprintf(out->launcher_part, sizeof(out->launcher_part), "%s.part", out->launcher_nro);
+ snprintf(out->next_load, sizeof(out->next_load), "%s", out->game_nro);
+ /* verified_temp is the game NRO; launcher replace uses a sibling verified path */
+ snprintf(out->steps[0], sizeof(out->steps[0]), "copy_to_part:%s->%s", verified_temp,
+ out->part_path);
+ snprintf(out->steps[1], sizeof(out->steps[1]), "rename:%s->%s", out->part_path, out->game_nro);
+ snprintf(out->steps[2], sizeof(out->steps[2]), "copy_to_part:launcher->%s", out->launcher_part);
+ snprintf(out->steps[3], sizeof(out->steps[3]), "rename:%s->%s", out->launcher_part,
+ out->launcher_nro);
+ snprintf(out->steps[4], sizeof(out->steps[4]), "env_set_next_load:%s", out->game_nro);
+ snprintf(out->preserve, sizeof(out->preserve), "%s/%s", install_dir, OTA_SAVE_DIR_NAME);
+ snprintf(out->forbidden_delete, sizeof(out->forbidden_delete), "delete:%s/%s", install_dir,
+ OTA_SAVE_DIR_NAME);
+ snprintf(out->forbidden_direct, sizeof(out->forbidden_direct), "write_direct:%s", out->game_nro);
+}
+
+void ota_offline_policy(double elapsed_sec, const ota_offline_events_t *events, ota_offline_t *out) {
+ memset(out, 0, sizeof(*out));
+ ota_offline_events_t ev = {0, -1, 0};
+ if (events) ev = *events;
+ if (ev.user_skip) {
+ snprintf(out->action, sizeof(out->action), "play_installed");
+ set_reason(out->reason, sizeof(out->reason), "user_skip");
+ snprintf(out->message, sizeof(out->message), "update skipped");
+ return;
+ }
+ if (ev.api_error || ev.network_ok == 0) {
+ snprintf(out->action, sizeof(out->action), "play_installed");
+ set_reason(out->reason, sizeof(out->reason), "offline_or_error");
+ snprintf(out->message, sizeof(out->message),
+ "offline or update check failed — play installed version");
+ return;
+ }
+ if (elapsed_sec >= (double)OTA_CHECK_TIMEOUT_SEC) {
+ snprintf(out->action, sizeof(out->action), "play_installed");
+ set_reason(out->reason, sizeof(out->reason), "timeout");
+ snprintf(out->message, sizeof(out->message), "update check timed out after %ds",
+ OTA_CHECK_TIMEOUT_SEC);
+ return;
+ }
+ snprintf(out->action, sizeof(out->action), "keep_checking");
+ set_reason(out->reason, sizeof(out->reason), "in_flight");
+}
diff --git a/ports/switch/ota-launcher/src/ota_ui.c b/ports/switch/ota-launcher/src/ota_ui.c
new file mode 100644
index 00000000..972310b1
--- /dev/null
+++ b/ports/switch/ota-launcher/src/ota_ui.c
@@ -0,0 +1,595 @@
+/*
+ * Switch OTA UI — matches in-game launcher language (Theme.lua):
+ * black field, RGB version rail, flat yellow/white buttons, white ink.
+ * Quiet until called. Uses framebuffer + 8x8 font + optional romfs logo.
+ */
+#include "ota_ui.h"
+
+#include
+#include
+#include
+
+#if defined(__SWITCH__)
+#include
+
+#include "../third_party/font8x8_basic.h"
+
+#define OTA_LOGO_ROMFS "romfs:/logo.rgba"
+
+#define FB_W 1280
+#define FB_H 720
+
+#define TITLE_Y 200
+#define LINE_TITLE 44
+#define LINE_BODY 36
+#define LINE_SMALL 28
+#define WRAP_MAX_PX 720
+
+/* Theme.PAL (0-255) -> RGBA8 */
+#define COL_BG RGBA8_MAXALPHA(0, 0, 0)
+#define COL_INK RGBA8_MAXALPHA(255, 255, 255)
+#define COL_DETAIL RGBA8_MAXALPHA(200, 200, 200)
+#define COL_MUTED RGBA8_MAXALPHA(150, 150, 150)
+#define COL_INVERSE RGBA8_MAXALPHA(0, 0, 0)
+#define COL_YELLOW RGBA8_MAXALPHA(255, 214, 0)
+#define COL_GREEN RGBA8_MAXALPHA(0, 255, 140)
+#define COL_RAIL_R RGBA8_MAXALPHA(255, 60, 72)
+#define COL_RAIL_B RGBA8_MAXALPHA(70, 150, 255)
+#define COL_RAIL_G RGBA8_MAXALPHA(255, 203, 5)
+#define COL_LINE RGBA8_MAXALPHA(90, 90, 90)
+#define COL_RAISED RGBA8_MAXALPHA(20, 20, 20)
+
+static int g_ready = 0;
+static Framebuffer g_fb;
+static PadState g_pad;
+static u32 *g_logo = NULL;
+static int g_logo_w = 0, g_logo_h = 0;
+
+static void fill_rect(u32 *fb, u32 stride_px, int x, int y, int w, int h, u32 color) {
+ if (w <= 0 || h <= 0) return;
+ if (x < 0) {
+ w += x;
+ x = 0;
+ }
+ if (y < 0) {
+ h += y;
+ y = 0;
+ }
+ if (x + w > FB_W) w = FB_W - x;
+ if (y + h > FB_H) h = FB_H - y;
+ if (w <= 0 || h <= 0) return;
+ for (int row = 0; row < h; row++) {
+ u32 *line = fb + (y + row) * stride_px + x;
+ for (int col = 0; col < w; col++) line[col] = color;
+ }
+}
+
+/* Normalize UTF-8 punctuation to ASCII; drop other non-ASCII bytes. */
+static size_t ota_ui_sanitize_ascii(const char *src, char *dst, size_t dst_len) {
+ if (!dst || dst_len == 0) return 0;
+ dst[0] = '\0';
+ if (!src) return 0;
+ size_t w = 0;
+ for (size_t i = 0; src[i] && w + 1 < dst_len;) {
+ unsigned char c = (unsigned char)src[i];
+ if (c < 0x80) {
+ dst[w++] = (char)c;
+ i++;
+ continue;
+ }
+ if (c == 0xE2 && src[i + 1] && src[i + 2]) {
+ unsigned char b2 = (unsigned char)src[i + 1];
+ unsigned char b3 = (unsigned char)src[i + 2];
+ if (b2 == 0x80 && b3 == 0xA6) {
+ if (w + 3 < dst_len) {
+ dst[w++] = '.';
+ dst[w++] = '.';
+ dst[w++] = '.';
+ }
+ i += 3;
+ continue;
+ }
+ if (b2 == 0x80 && (b3 == 0x94 || b3 == 0x93)) {
+ dst[w++] = '-';
+ i += 3;
+ continue;
+ }
+ if (b2 == 0x80 && (b3 == 0x98 || b3 == 0x99)) {
+ dst[w++] = '\'';
+ i += 3;
+ continue;
+ }
+ }
+ if ((c & 0xE0) == 0xC0) i += 2;
+ else if ((c & 0xF0) == 0xE0) i += 3;
+ else if ((c & 0xF8) == 0xF0) i += 4;
+ else i++;
+ }
+ dst[w] = '\0';
+ return w;
+}
+
+static void draw_glyph(u32 *fb, u32 stride_px, int x, int y, char ch, u32 color, int scale) {
+ unsigned char c = (unsigned char)ch;
+ if (c > 127) return;
+ const char *bits = font8x8_basic[c];
+ for (int row = 0; row < 8; row++) {
+ unsigned char line = (unsigned char)bits[row];
+ for (int col = 0; col < 8; col++) {
+ if (line & (1 << col)) {
+ fill_rect(fb, stride_px, x + col * scale, y + row * scale, scale, scale, color);
+ }
+ }
+ }
+}
+
+static int text_width(const char *s, int scale) {
+ if (!s) return 0;
+ return (int)strlen(s) * 8 * scale;
+}
+
+static void draw_text(u32 *fb, u32 stride_px, int x, int y, const char *s, u32 color, int scale) {
+ if (!s) return;
+ char buf[512];
+ ota_ui_sanitize_ascii(s, buf, sizeof(buf));
+ int cx = x;
+ for (const char *p = buf; *p; p++) {
+ if (*p == '\n') {
+ cx = x;
+ y += 8 * scale + 4;
+ continue;
+ }
+ draw_glyph(fb, stride_px, cx, y, *p, color, scale);
+ cx += 8 * scale;
+ }
+}
+
+static void draw_text_centered(u32 *fb, u32 stride_px, int y, const char *s, u32 color, int scale) {
+ char buf[512];
+ ota_ui_sanitize_ascii(s, buf, sizeof(buf));
+ int w = text_width(buf, scale);
+ draw_text(fb, stride_px, (FB_W - w) / 2, y, buf, color, scale);
+}
+
+static int draw_text_wrapped_centered(u32 *fb, u32 stride_px, int y, const char *s, u32 color,
+ int scale, int max_px) {
+ if (!s || !*s) return y;
+ char buf[512];
+ ota_ui_sanitize_ascii(s, buf, sizeof(buf));
+ int char_w = 8 * scale;
+ int max_chars = max_px / char_w;
+ if (max_chars < 8) max_chars = 8;
+
+ char line[128];
+ int line_len = 0;
+ const char *word = buf;
+ int lines = 0;
+
+ while (*word) {
+ while (*word == ' ') word++;
+ if (!*word) break;
+
+ const char *end = word;
+ while (*end && *end != ' ' && *end != '\n') end++;
+ int wlen = (int)(end - word);
+
+ if (line_len > 0 && line_len + 1 + wlen > max_chars) {
+ line[line_len] = '\0';
+ draw_text_centered(fb, stride_px, y, line, color, scale);
+ y += 8 * scale + 4;
+ lines++;
+ line_len = 0;
+ }
+
+ if (wlen > max_chars) {
+ if (line_len > 0) {
+ line[line_len] = '\0';
+ draw_text_centered(fb, stride_px, y, line, color, scale);
+ y += 8 * scale + 4;
+ lines++;
+ line_len = 0;
+ }
+ while (wlen > 0) {
+ int chunk = wlen > max_chars ? max_chars : wlen;
+ memcpy(line, word, (size_t)chunk);
+ line[chunk] = '\0';
+ draw_text_centered(fb, stride_px, y, line, color, scale);
+ y += 8 * scale + 4;
+ lines++;
+ word += chunk;
+ wlen -= chunk;
+ }
+ word = end;
+ continue;
+ }
+
+ if (line_len > 0) line[line_len++] = ' ';
+ memcpy(line + line_len, word, (size_t)wlen);
+ line_len += wlen;
+ line[line_len] = '\0';
+ word = (*end == '\n') ? end + 1 : end;
+ if (*(end - 1) == '\n' || *end == '\n') {
+ draw_text_centered(fb, stride_px, y, line, color, scale);
+ y += 8 * scale + 4;
+ lines++;
+ line_len = 0;
+ if (*end == '\n') word = end + 1;
+ }
+ }
+
+ if (line_len > 0) {
+ draw_text_centered(fb, stride_px, y, line, color, scale);
+ y += 8 * scale + 4;
+ lines++;
+ }
+ if (lines == 0) return y;
+ return y;
+}
+
+static void draw_rail(u32 *fb, u32 stride_px) {
+ int h = 6;
+ int third = FB_W / 3;
+ fill_rect(fb, stride_px, 0, 0, third, h, COL_RAIL_R);
+ fill_rect(fb, stride_px, third, 0, third, h, COL_RAIL_B);
+ fill_rect(fb, stride_px, third * 2, 0, FB_W - third * 2, h, COL_RAIL_G);
+}
+
+static void blit_logo(u32 *fb, u32 stride_px, int dst_x, int dst_y, int max_w) {
+ if (!g_logo || g_logo_w <= 0 || g_logo_h <= 0) return;
+ int dw = g_logo_w;
+ int dh = g_logo_h;
+ if (dw > max_w) {
+ dh = dh * max_w / dw;
+ dw = max_w;
+ }
+ for (int y = 0; y < dh; y++) {
+ int sy = y * g_logo_h / dh;
+ for (int x = 0; x < dw; x++) {
+ int sx = x * g_logo_w / dw;
+ u32 px = g_logo[sy * g_logo_w + sx];
+ u8 a = (px >> 24) & 0xff;
+ if (a < 16) continue;
+ int dx = dst_x + x;
+ int dy = dst_y + y;
+ if (dx < 0 || dy < 0 || dx >= FB_W || dy >= FB_H) continue;
+ fb[dy * stride_px + dx] = px | 0xff000000u;
+ }
+ }
+}
+
+static void load_logo(void) {
+ if (g_logo) return;
+ FILE *fp = fopen(OTA_LOGO_ROMFS, "rb");
+ if (!fp) return;
+
+ unsigned char header[8];
+ if (fread(header, 1, sizeof(header), fp) != sizeof(header)) {
+ fclose(fp);
+ return;
+ }
+
+ int w = (int)(header[0] | (header[1] << 8) | (header[2] << 16) | (header[3] << 24));
+ int h = (int)(header[4] | (header[5] << 8) | (header[6] << 16) | (header[7] << 24));
+ if (w <= 0 || h <= 0 || w > 2048 || h > 2048) {
+ fclose(fp);
+ return;
+ }
+
+ size_t nbytes = (size_t)w * (size_t)h * 4u;
+ unsigned char *data = (unsigned char *)malloc(nbytes);
+ if (!data) {
+ fclose(fp);
+ return;
+ }
+ if (fread(data, 1, nbytes, fp) != nbytes) {
+ free(data);
+ fclose(fp);
+ return;
+ }
+ fclose(fp);
+
+ g_logo = (u32 *)malloc((size_t)w * (size_t)h * sizeof(u32));
+ if (!g_logo) {
+ free(data);
+ return;
+ }
+ for (int i = 0; i < w * h; i++) {
+ unsigned char *p = data + (size_t)i * 4u;
+ g_logo[i] = RGBA8(p[0], p[1], p[2], p[3]);
+ }
+ free(data);
+ g_logo_w = w;
+ g_logo_h = h;
+}
+
+static int ui_ensure(void) {
+ if (g_ready) return 1;
+ NWindow *win = nwindowGetDefault();
+ if (R_FAILED(framebufferCreate(&g_fb, win, FB_W, FB_H, PIXEL_FORMAT_RGBA_8888, 2))) return 0;
+ framebufferMakeLinear(&g_fb);
+ padInitializeDefault(&g_pad);
+ load_logo();
+ g_ready = 1;
+ return 1;
+}
+
+void ota_ui_shutdown(void) {
+ if (!g_ready) return;
+ framebufferClose(&g_fb);
+ free(g_logo);
+ g_logo = NULL;
+ g_logo_w = g_logo_h = 0;
+ g_ready = 0;
+}
+
+static void draw_button(u32 *fb, u32 stride_px, int x, int y, int w, int h, u32 fill, u32 ink,
+ const char *label, int focused) {
+ fill_rect(fb, stride_px, x, y, w, h, fill);
+ if (focused) {
+ fill_rect(fb, stride_px, x - 3, y - 3, w + 6, 2, COL_INK);
+ fill_rect(fb, stride_px, x - 3, y + h + 1, w + 6, 2, COL_INK);
+ fill_rect(fb, stride_px, x - 3, y - 3, 2, h + 6, COL_INK);
+ fill_rect(fb, stride_px, x + w + 1, y - 3, 2, h + 6, COL_INK);
+ } else {
+ fill_rect(fb, stride_px, x, y, w, 1, COL_LINE);
+ fill_rect(fb, stride_px, x, y + h - 1, w, 1, COL_LINE);
+ fill_rect(fb, stride_px, x, y, 1, h, COL_LINE);
+ fill_rect(fb, stride_px, x + w - 1, y, 1, h, COL_LINE);
+ }
+ int scale = 3;
+ char lbl[128];
+ ota_ui_sanitize_ascii(label, lbl, sizeof(lbl));
+ int max_tw = w - 24;
+ int tw = text_width(lbl, scale);
+ while (scale > 2 && tw > max_tw) {
+ scale--;
+ tw = text_width(lbl, scale);
+ }
+ int tx = x + (w - tw) / 2;
+ int ty = y + (h - 8 * scale) / 2;
+ draw_text(fb, stride_px, tx, ty, lbl, ink, scale);
+}
+
+static void draw_chrome(u32 *fb, u32 stride_px) {
+ fill_rect(fb, stride_px, 0, 0, FB_W, FB_H, COL_BG);
+ draw_rail(fb, stride_px);
+ int logo_max = 320;
+ int logo_h = g_logo ? (g_logo_h * logo_max / g_logo_w) : 40;
+ if (logo_h > 90) logo_h = 90;
+ int logo_y = 48;
+ if (g_logo) {
+ int logo_w = logo_max;
+ if (g_logo_w * logo_h / g_logo_h < logo_max) logo_w = g_logo_w * logo_h / g_logo_h;
+ blit_logo(fb, stride_px, (FB_W - logo_w) / 2, logo_y, logo_max);
+ } else {
+ draw_text_centered(fb, stride_px, logo_y + 16, "gen1recomp", COL_INK, 4);
+ }
+}
+
+static void present(void (*paint)(u32 *fb, u32 stride_px, void *ctx), void *ctx) {
+ if (!ui_ensure()) return;
+ u32 stride = 0;
+ u32 *fb = (u32 *)framebufferBegin(&g_fb, &stride);
+ u32 stride_px = stride / sizeof(u32);
+ paint(fb, stride_px, ctx);
+ framebufferEnd(&g_fb);
+}
+
+typedef struct {
+ const char *installed;
+ const char *latest;
+ int focus; /* 0 = Update, 1 = Play */
+} prompt_ctx_t;
+
+static void paint_prompt(u32 *fb, u32 stride_px, void *ctx) {
+ prompt_ctx_t *p = (prompt_ctx_t *)ctx;
+ draw_chrome(fb, stride_px);
+
+ int y = TITLE_Y - 20;
+ draw_text_centered(fb, stride_px, y, "Update available", COL_INK, 3);
+ y += LINE_TITLE;
+
+ char line[96];
+ snprintf(line, sizeof(line), "v%s to v%s", p->installed ? p->installed : "?",
+ p->latest ? p->latest : "?");
+ draw_text_centered(fb, stride_px, y, line, COL_YELLOW, 3);
+ y += LINE_TITLE + 8;
+
+ draw_text_centered(fb, stride_px, y, "Your saves stay on this console.", COL_MUTED, 2);
+ y += LINE_BODY + 28;
+
+ int bw = 600;
+ int bh = 64;
+ int bx = (FB_W - bw) / 2;
+ draw_button(fb, stride_px, bx, y, bw, bh, COL_YELLOW, COL_INVERSE, "(A) Update", p->focus == 0);
+ y += bh + 24;
+ draw_button(fb, stride_px, bx, y, bw, bh, COL_INK, COL_INVERSE, "(B) Play without update",
+ p->focus == 1);
+}
+
+int ota_ui_prompt_update(const char *installed, const char *latest) {
+ if (!ui_ensure()) return 0;
+ prompt_ctx_t ctx = {installed, latest, 0};
+ while (appletMainLoop()) {
+ present(paint_prompt, &ctx);
+ padUpdate(&g_pad);
+ u64 k = padGetButtonsDown(&g_pad);
+ if (k & HidNpadButton_A) {
+ int do_update = (ctx.focus == 0);
+ if (!do_update) ota_ui_shutdown();
+ return do_update;
+ }
+ if (k & HidNpadButton_B) {
+ ota_ui_shutdown();
+ return 0;
+ }
+ if (k & (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_Left | HidNpadButton_Right)) {
+ ctx.focus = 1 - ctx.focus;
+ }
+ svcSleepThread(16000000ULL);
+ }
+ ota_ui_shutdown();
+ return 0;
+}
+
+typedef struct {
+ const char *title;
+ const char *detail;
+ float progress;
+ int show_bar;
+} status_ctx_t;
+
+static void paint_progress_bar(u32 *fb, u32 stride_px, int y, float t) {
+ int bw = 520;
+ int bh = 18;
+ int bx = (FB_W - bw) / 2;
+ if (t < 0.f) t = 0.f;
+ if (t > 1.f) t = 1.f;
+ fill_rect(fb, stride_px, bx, y, bw, bh, COL_RAISED);
+ fill_rect(fb, stride_px, bx, y, bw, 1, COL_LINE);
+ fill_rect(fb, stride_px, bx, y + bh - 1, bw, 1, COL_LINE);
+ fill_rect(fb, stride_px, bx, y, 1, bh, COL_LINE);
+ fill_rect(fb, stride_px, bx + bw - 1, y, 1, bh, COL_LINE);
+ int fw = (int)(bw * t);
+ if (fw > 0) fill_rect(fb, stride_px, bx, y, fw, bh, COL_GREEN);
+}
+
+static void paint_status(u32 *fb, u32 stride_px, void *ctx) {
+ status_ctx_t *s = (status_ctx_t *)ctx;
+ draw_chrome(fb, stride_px);
+ int y = TITLE_Y;
+ draw_text_centered(fb, stride_px, y, s->title ? s->title : "", COL_INK, 3);
+ y += LINE_TITLE;
+ if (s->detail && s->detail[0]) {
+ y = draw_text_wrapped_centered(fb, stride_px, y, s->detail, COL_DETAIL, 2, WRAP_MAX_PX);
+ y += LINE_SMALL;
+ }
+ if (s->show_bar) paint_progress_bar(fb, stride_px, y, s->progress);
+}
+
+void ota_ui_show_status(const char *title, const char *detail) {
+ status_ctx_t s = {title, detail, 0.f, 0};
+ present(paint_status, &s);
+}
+
+void ota_ui_show_progress(const char *title, const char *detail, float progress01) {
+ status_ctx_t s = {title, detail, progress01, 1};
+ present(paint_status, &s);
+}
+
+typedef struct {
+ const char *title;
+ const char *line1;
+ const char *line2;
+ const char *line3;
+} alert_ctx_t;
+
+static void paint_alert(u32 *fb, u32 stride_px, void *ctx) {
+ alert_ctx_t *a = (alert_ctx_t *)ctx;
+ draw_chrome(fb, stride_px);
+ int y = TITLE_Y;
+ draw_text_centered(fb, stride_px, y, a->title ? a->title : "", COL_YELLOW, 3);
+ y += LINE_TITLE;
+ if (a->line1 && a->line1[0])
+ y = draw_text_wrapped_centered(fb, stride_px, y, a->line1, COL_DETAIL, 2, WRAP_MAX_PX);
+ y += LINE_SMALL;
+ if (a->line2 && a->line2[0])
+ y = draw_text_wrapped_centered(fb, stride_px, y, a->line2, COL_MUTED, 2, WRAP_MAX_PX);
+ y += LINE_SMALL;
+ if (a->line3 && a->line3[0])
+ y = draw_text_wrapped_centered(fb, stride_px, y, a->line3, COL_MUTED, 2, WRAP_MAX_PX);
+ y += LINE_BODY + 16;
+ int bw = 360;
+ int bh = 56;
+ draw_button(fb, stride_px, (FB_W - bw) / 2, y, bw, bh, COL_INK, COL_INVERSE, "(B) Continue", 1);
+}
+
+void ota_ui_alert(const char *title, const char *line1, const char *line2) {
+ if (!ui_ensure()) return;
+ alert_ctx_t a = {title, line1, line2, NULL};
+ while (appletMainLoop()) {
+ present(paint_alert, &a);
+ padUpdate(&g_pad);
+ if (padGetButtonsDown(&g_pad) & HidNpadButton_B) break;
+ svcSleepThread(16000000ULL);
+ }
+ ota_ui_shutdown();
+}
+
+void ota_ui_alert_error(const char *title, const char *friendly, const char *technical,
+ const char *installed_version) {
+ if (!ui_ensure()) return;
+ char footer[96];
+ snprintf(footer, sizeof(footer), "Playing installed version (v%s).",
+ installed_version && installed_version[0] ? installed_version : "?");
+ char tech[256];
+ if (technical && technical[0]) {
+ ota_ui_sanitize_ascii(technical, tech, sizeof(tech));
+ if (strlen(tech) > 80) tech[80] = '\0';
+ } else {
+ tech[0] = '\0';
+ }
+ alert_ctx_t a = {title, friendly, tech[0] ? tech : NULL, footer};
+ while (appletMainLoop()) {
+ present(paint_alert, &a);
+ padUpdate(&g_pad);
+ if (padGetButtonsDown(&g_pad) & HidNpadButton_B) break;
+ svcSleepThread(16000000ULL);
+ }
+ ota_ui_shutdown();
+}
+
+static void paint_missing(u32 *fb, u32 stride_px, void *ctx) {
+ (void)ctx;
+ draw_chrome(fb, stride_px);
+ int y = TITLE_Y - 10;
+ draw_text_centered(fb, stride_px, y, "Game files missing", COL_YELLOW, 3);
+ y += LINE_TITLE;
+ y = draw_text_wrapped_centered(fb, stride_px, y,
+ "Copy the Switch zip onto your microSD, then open gen1recomp again.",
+ COL_DETAIL, 2, WRAP_MAX_PX);
+ y += LINE_BODY + 16;
+ int bw = 320;
+ int bh = 56;
+ draw_button(fb, stride_px, (FB_W - bw) / 2, y, bw, bh, COL_INK, COL_INVERSE, "(+) Exit", 1);
+}
+
+void ota_ui_missing_game(void) {
+ if (!ui_ensure()) return;
+ while (appletMainLoop()) {
+ present(paint_missing, NULL);
+ padUpdate(&g_pad);
+ if (padGetButtonsDown(&g_pad) & HidNpadButton_Plus) break;
+ svcSleepThread(16000000ULL);
+ }
+ ota_ui_shutdown();
+}
+
+#else /* host stub */
+
+int ota_ui_prompt_update(const char *installed, const char *latest) {
+ fprintf(stderr, "[ota_ui] update %s -> %s (host stub: skip)\n", installed ? installed : "?",
+ latest ? latest : "?");
+ return 0;
+}
+void ota_ui_show_status(const char *title, const char *detail) {
+ fprintf(stderr, "[ota_ui] %s %s\n", title ? title : "", detail ? detail : "");
+}
+void ota_ui_show_progress(const char *title, const char *detail, float progress01) {
+ fprintf(stderr, "[ota_ui] %s %s (%.0f%%)\n", title ? title : "", detail ? detail : "",
+ progress01 * 100.f);
+}
+void ota_ui_alert(const char *title, const char *line1, const char *line2) {
+ fprintf(stderr, "[ota_ui] alert: %s / %s / %s\n", title ? title : "", line1 ? line1 : "",
+ line2 ? line2 : "");
+}
+void ota_ui_alert_error(const char *title, const char *friendly, const char *technical,
+ const char *installed_version) {
+ fprintf(stderr, "[ota_ui] error: %s / %s / %s / v%s\n", title ? title : "",
+ friendly ? friendly : "", technical ? technical : "",
+ installed_version ? installed_version : "?");
+}
+void ota_ui_missing_game(void) { fprintf(stderr, "[ota_ui] missing game\n"); }
+void ota_ui_shutdown(void) {}
+
+#endif
diff --git a/ports/switch/ota-launcher/src/ota_unzip.c b/ports/switch/ota-launcher/src/ota_unzip.c
new file mode 100644
index 00000000..72c72425
--- /dev/null
+++ b/ports/switch/ota-launcher/src/ota_unzip.c
@@ -0,0 +1,89 @@
+#include "ota_unzip.h"
+
+#include
+#include
+
+#if defined(__SWITCH__)
+#include
+#include
+#endif
+
+#if defined(__SWITCH__)
+static int extract_member(ZZIP_DIR *dir, const char *member, const char *dest_path, char *err,
+ size_t err_len) {
+ ZZIP_FILE *zf = zzip_file_open(dir, member, O_RDONLY);
+ if (!zf) return 1; /* not found / cannot open */
+
+ FILE *out = fopen(dest_path, "wb");
+ if (!out) {
+ zzip_file_close(zf);
+ if (err && err_len) snprintf(err, err_len, "fopen dest failed: %s", dest_path);
+ return -1;
+ }
+
+ char buf[8192];
+ zzip_ssize_t n;
+ while ((n = zzip_file_read(zf, buf, sizeof(buf))) > 0) {
+ if ((zzip_ssize_t)fwrite(buf, 1, (size_t)n, out) != n) {
+ fclose(out);
+ zzip_file_close(zf);
+ if (err && err_len) snprintf(err, err_len, "fwrite failed");
+ return -1;
+ }
+ }
+ fclose(out);
+ zzip_file_close(zf);
+ if (n < 0) {
+ if (err && err_len) snprintf(err, err_len, "zzip_file_read failed");
+ return -1;
+ }
+ return 0;
+}
+#endif
+
+int ota_unzip_extract_file(const char *zip_path, const char *member_name, const char *dest_path,
+ char *err, size_t err_len) {
+ if (!zip_path || !member_name || !dest_path) {
+ if (err && err_len) snprintf(err, err_len, "bad args");
+ return -1;
+ }
+#if defined(__SWITCH__)
+ zzip_error_t zerr = ZZIP_NO_ERROR;
+ ZZIP_DIR *dir = zzip_dir_open(zip_path, &zerr);
+ if (!dir) {
+ if (err && err_len) snprintf(err, err_len, "zzip_dir_open failed (%d): %s", (int)zerr, zip_path);
+ return -1;
+ }
+
+ char alt1[192];
+ char alt2[192];
+ snprintf(alt1, sizeof(alt1), "switch/gen1recomp/%s", member_name);
+ snprintf(alt2, sizeof(alt2), "./%s", member_name);
+
+ const char *candidates[] = {member_name, alt1, alt2, NULL};
+ int rc = -1;
+ if (err && err_len) err[0] = '\0';
+ for (int i = 0; candidates[i]; i++) {
+ int t = extract_member(dir, candidates[i], dest_path, err, err_len);
+ if (t == 0) {
+ rc = 0;
+ break;
+ }
+ if (t < 0) {
+ rc = -1;
+ break;
+ }
+ }
+ if (rc != 0 && err && err_len && !err[0]) {
+ snprintf(err, err_len, "member not found in zip: %s", member_name);
+ }
+ zzip_dir_close(dir);
+ return rc;
+#else
+ (void)zip_path;
+ (void)member_name;
+ (void)dest_path;
+ if (err && err_len) snprintf(err, err_len, "ota_unzip only available on __SWITCH__");
+ return -1;
+#endif
+}
diff --git a/ports/switch/ota-launcher/third_party/font8x8_basic.h b/ports/switch/ota-launcher/third_party/font8x8_basic.h
new file mode 100644
index 00000000..125cf165
--- /dev/null
+++ b/ports/switch/ota-launcher/third_party/font8x8_basic.h
@@ -0,0 +1,152 @@
+/**
+ * 8x8 monochrome bitmap fonts for rendering
+ * Author: Daniel Hepper
+ *
+ * License: Public Domain
+ *
+ * Based on:
+ * // Summary: font8x8.h
+ * // 8x8 monochrome bitmap fonts for rendering
+ * //
+ * // Author:
+ * // Marcel Sondaar
+ * // International Business Machines (public domain VGA fonts)
+ * //
+ * // License:
+ * // Public Domain
+ *
+ * Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm
+ **/
+
+// Constant: font8x8_basic
+// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin)
+char font8x8_basic[128][8] = {
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul)
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space)
+ { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!)
+ { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (")
+ { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#)
+ { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($)
+ { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%)
+ { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&)
+ { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (')
+ { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (()
+ { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ())
+ { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*)
+ { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+)
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,)
+ { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-)
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.)
+ { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/)
+ { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0)
+ { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1)
+ { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2)
+ { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3)
+ { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4)
+ { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5)
+ { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6)
+ { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7)
+ { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8)
+ { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9)
+ { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:)
+ { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;)
+ { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<)
+ { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=)
+ { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>)
+ { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?)
+ { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@)
+ { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A)
+ { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B)
+ { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C)
+ { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D)
+ { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E)
+ { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F)
+ { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G)
+ { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H)
+ { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I)
+ { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J)
+ { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K)
+ { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L)
+ { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M)
+ { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N)
+ { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O)
+ { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P)
+ { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q)
+ { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R)
+ { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S)
+ { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T)
+ { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U)
+ { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V)
+ { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W)
+ { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X)
+ { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y)
+ { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z)
+ { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([)
+ { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\)
+ { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (])
+ { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^)
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_)
+ { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`)
+ { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a)
+ { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b)
+ { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c)
+ { 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d)
+ { 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e)
+ { 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f)
+ { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g)
+ { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h)
+ { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i)
+ { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j)
+ { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k)
+ { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l)
+ { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m)
+ { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n)
+ { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o)
+ { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p)
+ { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q)
+ { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r)
+ { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s)
+ { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t)
+ { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u)
+ { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v)
+ { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w)
+ { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x)
+ { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y)
+ { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z)
+ { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({)
+ { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|)
+ { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (})
+ { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~)
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F
+};
diff --git a/ports/uwp/Assets/LargeTile.png b/ports/uwp/Assets/LargeTile.png
new file mode 100644
index 00000000..f9b2bafb
Binary files /dev/null and b/ports/uwp/Assets/LargeTile.png differ
diff --git a/ports/uwp/Assets/SmallTile.png b/ports/uwp/Assets/SmallTile.png
new file mode 100644
index 00000000..07af1d9f
Binary files /dev/null and b/ports/uwp/Assets/SmallTile.png differ
diff --git a/ports/uwp/Assets/SplashScreen.png b/ports/uwp/Assets/SplashScreen.png
new file mode 100644
index 00000000..5e938064
Binary files /dev/null and b/ports/uwp/Assets/SplashScreen.png differ
diff --git a/ports/uwp/Assets/Square150x150Logo.png b/ports/uwp/Assets/Square150x150Logo.png
new file mode 100644
index 00000000..66f7e44f
Binary files /dev/null and b/ports/uwp/Assets/Square150x150Logo.png differ
diff --git a/ports/uwp/Assets/Square44x44Logo.png b/ports/uwp/Assets/Square44x44Logo.png
new file mode 100644
index 00000000..adf2102e
Binary files /dev/null and b/ports/uwp/Assets/Square44x44Logo.png differ
diff --git a/ports/uwp/Assets/StoreLogo.png b/ports/uwp/Assets/StoreLogo.png
new file mode 100644
index 00000000..929abe5c
Binary files /dev/null and b/ports/uwp/Assets/StoreLogo.png differ
diff --git a/ports/uwp/Assets/WideTile.png b/ports/uwp/Assets/WideTile.png
new file mode 100644
index 00000000..8f30a761
Binary files /dev/null and b/ports/uwp/Assets/WideTile.png differ
diff --git a/ports/uwp/BUILD.md b/ports/uwp/BUILD.md
new file mode 100644
index 00000000..40cddc14
--- /dev/null
+++ b/ports/uwp/BUILD.md
@@ -0,0 +1,124 @@
+# Gen1Recomp Xbox UWP build notes
+
+This is the Xbox Dev Mode package for Gen1Recomp.
+
+The rough shape is:
+
+- `Gen1RecompUWP.exe` starts LÖVE through SDL's WinRT wrapper
+- the bundled LÖVE 11.5 UWP backend provides LuaJIT and the Xbox file picker
+- the bundled SDL2 runtime contains the Xbox controller mapping
+- ANGLE provides OpenGL ES over D3D11
+- the bundled runtime contains the audio, font, video, and compression libraries
+
+## What You Need
+
+The tested toolchain is:
+
+- Visual Studio 2022 17.14
+- MSVC v143 x64/x86 build tools
+- C++ Universal Windows Platform tools
+- Windows 11 SDK `10.0.26100.0`
+- CMake 3.24 or newer
+- Git for Windows
+- Info-ZIP `zip` and `unzip`
+
+Use Visual Studio Installer to add **Universal Windows Platform development**, the v143 C++ tools, CMake tools for Windows, and Windows SDK `10.0.26100.0`.
+
+The x64 UWP dependencies are committed under `third_party`. Their versions,
+source revisions, licences, and hashes are recorded in `third_party/manifest.json`.
+No additional checkout or environment variable is required for a normal game
+build.
+
+## Rebuild the Dependencies
+
+Run the dependency rebuild from the repository root:
+
+```powershell
+.\scripts\xbox-uwp\rebuild_dependencies.ps1
+```
+
+The script clones the pinned SDL2, LÖVE, LuaJIT, vcpkg, depot_tools, and ANGLE
+sources when they are missing. It applies the Xbox SDL2 patch, builds the x64
+UWP Release libraries, stages the required DLLs, import libraries, headers, and
+licences under `third_party`, updates every SHA-256 entry in the manifest, then
+builds the Release MSIX.
+
+The generated source checkouts are ignored by Git. A fresh ANGLE sync is about
+10 GB, so allow at least 20 GB of free disk space for all sources and build
+outputs. Use `-SkipAngle` to retain the existing pinned ANGLE runtime while
+rebuilding SDL2, LÖVE, LuaJIT, and the vcpkg libraries. Use `-SkipPackage` when
+only the dependency bundle needs to be refreshed. The rebuild stops if a source
+checkout has local changes. Remove that generated `source` directory to restore
+the pinned revision.
+
+## Build the MSIX
+
+Run the Xbox build from Git Bash at the repository root:
+
+```bash
+scripts/build_xbox_uwp.sh --release --version 1.2.3
+```
+
+The build uses `scripts/pack_love.sh` to create and verify the same ROM-free
+`game.love` payload used by the other release targets. It then links the UWP
+host and stages LÖVE, LuaJIT, SDL2, ANGLE, and the vcpkg runtime DLLs.
+
+Use `--relwithdebinfo` for a package with symbols. To package a `.love` produced
+by another build or downloaded from CI, pass `--game-love path/to/game.love`.
+The upstream `X.Y.Z` release becomes `X.Y.Z.0` in the generated MSIX manifest.
+Neither the manifest template nor `src/core/Version.lua` is edited in place.
+
+The manifest publisher must match the signing certificate subject. Pass it
+when preparing a signed package:
+
+```bash
+scripts/build_xbox_uwp.sh --release --version 1.2.3 \
+ --publisher "CN=Gen1Recomp"
+```
+
+The normal build is unsigned. Release CI supplies the private PFX and password
+from `XBOX_UWP_SIGNING_CERTIFICATE` and `XBOX_UWP_SIGNING_PASSWORD`; neither may
+be committed. The public certificate is safe to include with the release.
+
+Run the offline packaging checks from Git Bash:
+
+```bash
+bash scripts/xbox-uwp/selftest_build_xbox_uwp.sh
+```
+
+## Build Output
+
+Visual Studio package output lands under:
+
+```text
+ports\uwp\build\release\AppPackages\Gen1RecompUWP
+```
+
+The build also stages the distributable archive and checksum under:
+
+```text
+dist\xbox-uwp\gen1recomp-X.Y.Z-xbox-uwp.zip
+dist\xbox-uwp\gen1recomp-X.Y.Z-xbox-uwp.zip.sha256
+```
+
+The archive contains the MSIX, framework dependencies, build provenance and,
+for a signed release, the public certificate. The third-party notices are
+packaged inside the MSIX. Install the MSIX and dependency packages through
+Xbox Device Portal.
+
+## Runtime Data
+
+The package contains no ROM, generated cache, save or mod data. The Xbox file
+picker copies user-selected files into LocalState and the launcher imports them
+from there. Saves, ROM cache and installed mods remain under the LÖVE save
+directory in LocalState.
+
+The launcher also checks `LocalState\pokemon-love2d\baseroms` once at startup
+for clean Red, Blue, and Yellow ROMs. Compatible files are offered on their
+launcher tabs and remain in `baseroms` after import. The file picker remains
+available when no compatible ROM is found.
+
+LuaJIT requires the `codeGeneration` capability. `removableStorage` exposes
+external media to the Xbox picker. The network capabilities support relay play
+and direct hosting. The package does not request full trust or broad filesystem
+access.
diff --git a/ports/uwp/CMakeLists.txt b/ports/uwp/CMakeLists.txt
new file mode 100644
index 00000000..ed62ba93
--- /dev/null
+++ b/ports/uwp/CMakeLists.txt
@@ -0,0 +1,149 @@
+cmake_minimum_required(VERSION 3.24)
+project(Gen1RecompUWP LANGUAGES CXX)
+
+if(NOT CMAKE_SYSTEM_NAME STREQUAL "WindowsStore")
+ message(FATAL_ERROR "Configure with a WindowsStore preset.")
+endif()
+
+get_filename_component(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
+set(THIRD_PARTY_ROOT "${CMAKE_CURRENT_LIST_DIR}/third_party")
+set(LOVE_ROOT "${THIRD_PARTY_ROOT}/love")
+set(SDL2_ROOT "${THIRD_PARTY_ROOT}/sdl2")
+set(ANGLE_ROOT "${THIRD_PARTY_ROOT}/angle")
+set(RUNTIME_ROOT "${THIRD_PARTY_ROOT}/runtime")
+
+set(GEN1RECOMP_VERSION "0.0.0" CACHE STRING "Gen1Recomp release version")
+if(NOT GEN1RECOMP_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$")
+ message(FATAL_ERROR "GEN1RECOMP_VERSION must use X.Y.Z format.")
+endif()
+string(REPLACE "." ";" VERSION_PARTS "${GEN1RECOMP_VERSION}")
+foreach(part IN LISTS VERSION_PARTS)
+ if(part GREATER 65535)
+ message(FATAL_ERROR "MSIX version components cannot exceed 65535.")
+ endif()
+endforeach()
+set(GEN1RECOMP_UWP_PUBLISHER "CN=Gen1Recomp" CACHE STRING
+ "Publisher subject from the MSIX signing certificate")
+set(UWP_PUBLISHER_XML "${GEN1RECOMP_UWP_PUBLISHER}")
+string(REPLACE "&" "&" UWP_PUBLISHER_XML "${UWP_PUBLISHER_XML}")
+string(REPLACE "\"" """ UWP_PUBLISHER_XML "${UWP_PUBLISHER_XML}")
+string(REPLACE "<" "<" UWP_PUBLISHER_XML "${UWP_PUBLISHER_XML}")
+string(REPLACE ">" ">" UWP_PUBLISHER_XML "${UWP_PUBLISHER_XML}")
+set(UWP_PACKAGE_VERSION "${GEN1RECOMP_VERSION}.0")
+set(PACKAGE_MANIFEST "${CMAKE_CURRENT_BINARY_DIR}/Package.appxmanifest")
+configure_file(
+ "${CMAKE_CURRENT_LIST_DIR}/Package.appxmanifest.in"
+ "${PACKAGE_MANIFEST}"
+ @ONLY
+)
+
+set(REQUIRED_FILES
+ "${THIRD_PARTY_ROOT}/manifest.json"
+ "${LOVE_ROOT}/lib/lovestatic.lib"
+ "${LOVE_ROOT}/lib/liblove.lib"
+ "${LOVE_ROOT}/lib/lua51.lib"
+ "${LOVE_ROOT}/bin/love.dll"
+ "${LOVE_ROOT}/bin/lua51.dll"
+ "${SDL2_ROOT}/include/SDL2/SDL.h"
+ "${SDL2_ROOT}/lib/SDL2.lib"
+ "${SDL2_ROOT}/bin/SDL2.dll"
+ "${ANGLE_ROOT}/bin/libEGL.dll"
+ "${ANGLE_ROOT}/bin/libGLESv2.dll"
+ "${ANGLE_ROOT}/bin/d3dcompiler_47.dll"
+)
+foreach(path IN LISTS REQUIRED_FILES)
+ if(NOT EXISTS "${path}")
+ message(FATAL_ERROR "Missing UWP dependency: ${path}")
+ endif()
+endforeach()
+
+set(GEN1RECOMP_LOVE "${GAME_ROOT}/.bazinga/work/game.love" CACHE FILEPATH
+ "Path to the game.love payload produced by scripts/pack_love.sh")
+if(NOT EXISTS "${GEN1RECOMP_LOVE}")
+ message(FATAL_ERROR
+ "Missing game.love payload: ${GEN1RECOMP_LOVE}\n"
+ "Build it with scripts/build_xbox_uwp.sh or scripts/pack_love.sh.")
+endif()
+
+set(GAME_ARCHIVE "${CMAKE_CURRENT_BINARY_DIR}/gen1recomp.love")
+add_custom_command(
+ OUTPUT "${GAME_ARCHIVE}"
+ COMMAND "${CMAKE_COMMAND}" -E copy_if_different
+ "${GEN1RECOMP_LOVE}" "${GAME_ARCHIVE}"
+ DEPENDS "${GEN1RECOMP_LOVE}"
+ VERBATIM
+)
+add_custom_target(gen1recomp_love ALL DEPENDS "${GAME_ARCHIVE}")
+set_source_files_properties("${GAME_ARCHIVE}" PROPERTIES GENERATED TRUE)
+
+add_custom_target(verify_uwp_dependencies
+ COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File
+ "${GAME_ROOT}/scripts/xbox-uwp/verify_dependencies.ps1"
+ VERBATIM
+)
+
+add_executable(${PROJECT_NAME} WIN32 "app/main.cpp")
+add_dependencies(${PROJECT_NAME} gen1recomp_love verify_uwp_dependencies)
+set_target_properties(${PROJECT_NAME} PROPERTIES
+ CXX_STANDARD 17
+ CXX_STANDARD_REQUIRED YES
+ VS_GLOBAL_DefaultLanguage "en-US"
+ VS_SDK_REFERENCES "Microsoft.VCLibs, Version=14.0"
+)
+target_include_directories(${PROJECT_NAME} PRIVATE "${SDL2_ROOT}/include/SDL2")
+target_link_libraries(${PROJECT_NAME} PRIVATE
+ "${LOVE_ROOT}/lib/lovestatic.lib"
+ "${LOVE_ROOT}/lib/liblove.lib"
+ "${SDL2_ROOT}/lib/SDL2.lib"
+ "${LOVE_ROOT}/lib/lua51.lib"
+ WindowsApp.lib
+)
+
+set(PACKAGE_ROOT_FILES
+ "${PACKAGE_MANIFEST}"
+ "${GAME_ARCHIVE}"
+ "${LOVE_ROOT}/bin/love.dll"
+ "${LOVE_ROOT}/bin/lua51.dll"
+ "${SDL2_ROOT}/bin/SDL2.dll"
+ "${ANGLE_ROOT}/bin/libEGL.dll"
+ "${ANGLE_ROOT}/bin/libGLESv2.dll"
+ "${ANGLE_ROOT}/bin/d3dcompiler_47.dll"
+)
+set_source_files_properties("${PACKAGE_MANIFEST}" PROPERTIES GENERATED TRUE)
+
+set(RUNTIME_NAMES
+ brotlicommon.dll brotlidec.dll bz2.dll fmt.dll freetype.dll libpng16.dll
+ OpenAL32.dll theora.dll theoradec.dll vorbis.dll vorbisfile.dll ogg.dll z.dll
+)
+foreach(name IN LISTS RUNTIME_NAMES)
+ set(runtime "${RUNTIME_ROOT}/bin/${name}")
+ if(NOT EXISTS "${runtime}")
+ message(FATAL_ERROR "Missing UWP runtime DLL: ${name}")
+ endif()
+ list(APPEND PACKAGE_ROOT_FILES "${runtime}")
+endforeach()
+
+set_source_files_properties(${PACKAGE_ROOT_FILES} PROPERTIES
+ VS_COPY_TO_OUT_DIR Always
+ VS_DEPLOYMENT_CONTENT TRUE
+ VS_DEPLOYMENT_LOCATION "."
+)
+
+file(GLOB PACKAGE_ASSETS CONFIGURE_DEPENDS "${CMAKE_CURRENT_LIST_DIR}/Assets/*.png")
+set_source_files_properties(${PACKAGE_ASSETS} PROPERTIES
+ VS_DEPLOYMENT_CONTENT TRUE
+ VS_DEPLOYMENT_LOCATION "Assets"
+)
+
+file(GLOB PACKAGE_LICENSES CONFIGURE_DEPENDS "${THIRD_PARTY_ROOT}/licenses/*.txt")
+set_source_files_properties(${PACKAGE_LICENSES} PROPERTIES
+ VS_TOOL_OVERRIDE "Content"
+ VS_COPY_TO_OUT_DIR Always
+ VS_DEPLOYMENT_CONTENT TRUE
+ VS_DEPLOYMENT_LOCATION "licenses"
+)
+target_sources(${PROJECT_NAME} PRIVATE
+ ${PACKAGE_ROOT_FILES}
+ ${PACKAGE_ASSETS}
+ ${PACKAGE_LICENSES}
+)
diff --git a/ports/uwp/CMakePresets.json b/ports/uwp/CMakePresets.json
new file mode 100644
index 00000000..a97dcca6
--- /dev/null
+++ b/ports/uwp/CMakePresets.json
@@ -0,0 +1,41 @@
+{
+ "version": 6,
+ "configurePresets": [
+ {
+ "name": "uwp-common",
+ "hidden": true,
+ "generator": "Visual Studio 17 2022",
+ "architecture": "x64",
+ "cacheVariables": {
+ "CMAKE_SYSTEM_NAME": "WindowsStore",
+ "CMAKE_SYSTEM_VERSION": "10.0"
+ }
+ },
+ {
+ "name": "uwp-relwithdebinfo",
+ "inherits": "uwp-common",
+ "displayName": "Gen1Recomp Xbox UWP (RelWithDebInfo)",
+ "binaryDir": "${sourceDir}/build/relwithdebinfo"
+ },
+ {
+ "name": "uwp-release",
+ "inherits": "uwp-common",
+ "displayName": "Gen1Recomp Xbox UWP (Release)",
+ "binaryDir": "${sourceDir}/build/release"
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "uwp-relwithdebinfo",
+ "configurePreset": "uwp-relwithdebinfo",
+ "configuration": "RelWithDebInfo",
+ "jobs": 8
+ },
+ {
+ "name": "uwp-release",
+ "configurePreset": "uwp-release",
+ "configuration": "Release",
+ "jobs": 8
+ }
+ ]
+}
diff --git a/ports/uwp/Package.appxmanifest.in b/ports/uwp/Package.appxmanifest.in
new file mode 100644
index 00000000..1aba5668
--- /dev/null
+++ b/ports/uwp/Package.appxmanifest.in
@@ -0,0 +1,43 @@
+
+
+
+
+
+ Gen1Recomp
+ Gen1Recomp
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ports/uwp/app/main.cpp b/ports/uwp/app/main.cpp
new file mode 100644
index 00000000..1f5e8b77
--- /dev/null
+++ b/ports/uwp/app/main.cpp
@@ -0,0 +1,30 @@
+#include
+#include
+#include
+#include
+#include
+
+extern "C" int SDL_main(int argc, char **argv);
+
+namespace
+{
+
+int runLove(int, char **)
+{
+ std::wstring packagePath = winrt::Windows::ApplicationModel::Package::Current()
+ .InstalledLocation().Path().c_str();
+ std::string gamePath = winrt::to_string(packagePath + L"\\gen1recomp.love");
+
+ char executable[] = "Gen1RecompUWP";
+ char fused[] = "--fused";
+ char *loveArgv[] = {executable, gamePath.data(), fused, nullptr};
+ return SDL_main(3, loveArgv);
+}
+
+} // namespace
+
+int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
+{
+ SDL_SetHint(SDL_HINT_WINRT_HANDLE_BACK_BUTTON, "1");
+ return SDL_WinRTRunApp(runLove, nullptr);
+}
diff --git a/ports/uwp/third_party/README.md b/ports/uwp/third_party/README.md
new file mode 100644
index 00000000..1441d685
--- /dev/null
+++ b/ports/uwp/third_party/README.md
@@ -0,0 +1,11 @@
+# UWP dependencies
+
+This directory contains the complete x64 UWP Release dependency bundle used by the package build:
+
+- `love` contains the LÖVE 11.5 and LuaJIT binaries.
+- `sdl2` contains the matching SDL headers, import library, and runtime.
+- `angle` contains the EGL and GLES runtime.
+- `runtime` contains the codec, font, compression, and audio DLLs used by LÖVE.
+- `licenses` contains the corresponding third party notices.
+
+`manifest.json` pins the source revisions and SHA-256 hashes. Run `scripts/xbox-uwp/verify_dependencies.ps1` from the repository root after updating any dependency.
diff --git a/ports/uwp/third_party/angle/AUTHORS b/ports/uwp/third_party/angle/AUTHORS
new file mode 100644
index 00000000..37a9c07d
--- /dev/null
+++ b/ports/uwp/third_party/angle/AUTHORS
@@ -0,0 +1,89 @@
+# This is the official list of The ANGLE Project Authors
+# for copyright purposes.
+# This file is distinct from the CONTRIBUTORS files.
+# See the latter for an explanation.
+
+# Names should be added to this file as
+# Name or Organization
+# Email addresses for individuals are tracked elsewhere to avoid spam.
+
+Google Inc.
+TransGaming Inc.
+3DLabs Inc. Ltd.
+
+Adobe Systems Inc.
+Autodesk, Inc.
+BlackBerry Limited
+Cable Television Laboratories, Inc.
+Collabora, Ltd.
+Cloud Party, Inc.
+Igalia, S.L.
+Imagination Technologies Ltd.
+Intel Corporation
+LunarG, Inc.
+Mozilla Corporation
+Turbulenz
+Klarälvdalens Datakonsult AB
+Microsoft Corporation
+Microsoft Open Technologies, Inc.
+NVIDIA Corporation
+Opera Software ASA
+The Qt Company Ltd.
+Advanced Micro Devices, Inc.
+LG Electronics, Inc.
+IBM Inc.
+AdaptVis GmbH
+Samsung Electronics, Inc.
+Arm Ltd.
+Broadcom Inc.
+Facebook, Inc.
+The Khronos Group, Inc.
+Numfum GmbH
+Yandex LLC
+Rive
+Institute of Software, Chinese Academy of Sciences
+Guangdong OPPO Mobile Telecommunications Corp., Ltd
+Qualcomm Innovation Center, Inc.
+
+Jacek Caban
+Mark Callow
+Ginn Chen
+Tibor den Ouden
+Régis Fénéon
+James Hauxwell
+Sam Hocevar
+Pierre Leveille
+Jonathan Liu
+Boying Lu
+Aitor Moreno
+Yuri O'Donnell
+Josh Soref
+Ma Aiguo
+Maks Naumov
+Jinyoung Hur
+Sebastian Bergstein
+James Ross-Gowan
+Nickolay Artamonov
+Ihsan Akmal
+Andrei Volykhin
+Jérôme Duval
+Руслан Ижбулатов
+Thomas Miller
+Till Rathmann
+Nick Shaforostov
+Jaime Bernardo
+Le Hoang Quyen
+Lu Yahan
+Ethan Lee
+Renaud Lepage
+Artem Bolgar
+Wander Lairson Costa
+Stephan Hartmann
+SeongHwan Park
+Xiaopeng Li
+Akihiko Odaki
+Ho Cheung
+Tao Wang
+Phan Quang Minh
+Hongchen Yan
+Andrew Sumsion
diff --git a/ports/uwp/third_party/angle/LICENSE b/ports/uwp/third_party/angle/LICENSE
new file mode 100644
index 00000000..0f65fd60
--- /dev/null
+++ b/ports/uwp/third_party/angle/LICENSE
@@ -0,0 +1,32 @@
+// Copyright 2018 The ANGLE Project Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+// Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+//
+// Neither the name of TransGaming Inc., Google Inc., 3DLabs Inc.
+// Ltd., nor the names of their contributors may be used to endorse
+// or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
diff --git a/ports/uwp/third_party/angle/README.md b/ports/uwp/third_party/angle/README.md
new file mode 100644
index 00000000..722f0f4a
--- /dev/null
+++ b/ports/uwp/third_party/angle/README.md
@@ -0,0 +1,7 @@
+# ANGLE UWP runtime
+
+`libEGL.dll` and `libGLESv2.dll` were built for x64 UWP from [SternXD/angle](https://github.com/SternXD/angle) commit `45b0b1e03400b7a10aaa9a077e196d1abcddafce`. ANGLE version `2.1.25011` and source hash `45b0b1e03400`.
+
+`d3dcompiler_47.dll` is the x64 Direct3D HLSL compiler redistributable from Windows SDK `10.0.26100.7705`. Current hashes are recorded in `../manifest.json`.
+
+ANGLE's upstream `LICENSE` and `AUTHORS` files are included beside its binaries.
diff --git a/ports/uwp/third_party/angle/bin/d3dcompiler_47.dll b/ports/uwp/third_party/angle/bin/d3dcompiler_47.dll
new file mode 100644
index 00000000..948cc90b
Binary files /dev/null and b/ports/uwp/third_party/angle/bin/d3dcompiler_47.dll differ
diff --git a/ports/uwp/third_party/angle/bin/libEGL.dll b/ports/uwp/third_party/angle/bin/libEGL.dll
new file mode 100644
index 00000000..111392f6
Binary files /dev/null and b/ports/uwp/third_party/angle/bin/libEGL.dll differ
diff --git a/ports/uwp/third_party/angle/bin/libGLESv2.dll b/ports/uwp/third_party/angle/bin/libGLESv2.dll
new file mode 100644
index 00000000..e54e7fc1
Binary files /dev/null and b/ports/uwp/third_party/angle/bin/libGLESv2.dll differ
diff --git a/ports/uwp/third_party/licenses/angle.txt b/ports/uwp/third_party/licenses/angle.txt
new file mode 100644
index 00000000..0f65fd60
--- /dev/null
+++ b/ports/uwp/third_party/licenses/angle.txt
@@ -0,0 +1,32 @@
+// Copyright 2018 The ANGLE Project Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+//
+// Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+//
+// Neither the name of TransGaming Inc., Google Inc., 3DLabs Inc.
+// Ltd., nor the names of their contributors may be used to endorse
+// or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
diff --git a/ports/uwp/third_party/licenses/brotli.txt b/ports/uwp/third_party/licenses/brotli.txt
new file mode 100644
index 00000000..33b7cdd2
--- /dev/null
+++ b/ports/uwp/third_party/licenses/brotli.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/ports/uwp/third_party/licenses/bzip2.txt b/ports/uwp/third_party/licenses/bzip2.txt
new file mode 100644
index 00000000..81a37eab
--- /dev/null
+++ b/ports/uwp/third_party/licenses/bzip2.txt
@@ -0,0 +1,42 @@
+
+--------------------------------------------------------------------------
+
+This program, "bzip2", the associated library "libbzip2", and all
+documentation, are copyright (C) 1996-2019 Julian R Seward. All
+rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. The origin of this software must not be misrepresented; you must
+ not claim that you wrote the original software. If you use this
+ software in a product, an acknowledgment in the product
+ documentation would be appreciated but is not required.
+
+3. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+4. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Julian Seward, jseward@acm.org
+bzip2/libbzip2 version 1.0.8 of 13 July 2019
+
+--------------------------------------------------------------------------
diff --git a/ports/uwp/third_party/licenses/fmt.txt b/ports/uwp/third_party/licenses/fmt.txt
new file mode 100644
index 00000000..1cd1ef92
--- /dev/null
+++ b/ports/uwp/third_party/licenses/fmt.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into a machine-executable object form of such
+source code, you may redistribute such embedded portions in such object form
+without including the above copyright and permission notices.
diff --git a/ports/uwp/third_party/licenses/freetype.txt b/ports/uwp/third_party/licenses/freetype.txt
new file mode 100644
index 00000000..6289a620
--- /dev/null
+++ b/ports/uwp/third_party/licenses/freetype.txt
@@ -0,0 +1,568 @@
+LICENSE.TXT:
+
+FREETYPE LICENSES
+-----------------
+
+The FreeType 2 font engine is copyrighted work and cannot be used
+legally without a software license. In order to make this project
+usable to a vast majority of developers, we distribute it under two
+mutually exclusive open-source licenses.
+
+This means that *you* must choose *one* of the two licenses described
+below, then obey all its terms and conditions when using FreeType 2 in
+any of your projects or products.
+
+ - The FreeType License, found in the file `docs/FTL.TXT`, which is
+ similar to the original BSD license *with* an advertising clause
+ that forces you to explicitly cite the FreeType project in your
+ product's documentation. All details are in the license file.
+ This license is suited to products which don't use the GNU General
+ Public License.
+
+ Note that this license is compatible to the GNU General Public
+ License version 3, but not version 2.
+
+ - The GNU General Public License version 2, found in
+ `docs/GPLv2.TXT` (any later version can be used also), for
+ programs which already use the GPL. Note that the FTL is
+ incompatible with GPLv2 due to its advertisement clause.
+
+The contributed BDF and PCF drivers come with a license similar to
+that of the X Window System. It is compatible to the above two
+licenses (see files `src/bdf/README` and `src/pcf/README`). The same
+holds for the source code files `src/base/fthash.c` and
+`include/freetype/internal/fthash.h`; they were part of the BDF driver
+in earlier FreeType versions.
+
+The gzip module uses the zlib license (see `src/gzip/zlib.h`) which
+too is compatible to the above two licenses.
+
+The files `src/autofit/ft-hb-ft.c`, `src/autofit/ft-hb-decls.h`,
+`src/autofit/ft-hb-types.h`, and `src/autofit/hb-script-list.h`
+contain code taken (almost) verbatim from the HarfBuzz library, which
+uses the 'Old MIT' license compatible to the above two licenses.
+
+The MD5 checksum support (only used for debugging in development
+builds) is in the public domain.
+
+
+--- end of LICENSE.TXT ---
+
+
+FTL.TXT:
+
+ The FreeType Project LICENSE
+ ----------------------------
+
+ 2006-Jan-27
+
+ Copyright 1996-2002, 2006 by
+ David Turner, Robert Wilhelm, and Werner Lemberg
+
+
+
+Introduction
+============
+
+ The FreeType Project is distributed in several archive packages;
+ some of them may contain, in addition to the FreeType font engine,
+ various tools and contributions which rely on, or relate to, the
+ FreeType Project.
+
+ This license applies to all files found in such packages, and
+ which do not fall under their own explicit license. The license
+ affects thus the FreeType font engine, the test programs,
+ documentation and makefiles, at the very least.
+
+ This license was inspired by the BSD, Artistic, and IJG
+ (Independent JPEG Group) licenses, which all encourage inclusion
+ and use of free software in commercial and freeware products
+ alike. As a consequence, its main points are that:
+
+ o We don't promise that this software works. However, we will be
+ interested in any kind of bug reports. (`as is' distribution)
+
+ o You can use this software for whatever you want, in parts or
+ full form, without having to pay us. (`royalty-free' usage)
+
+ o You may not pretend that you wrote this software. If you use
+ it, or only parts of it, in a program, you must acknowledge
+ somewhere in your documentation that you have used the
+ FreeType code. (`credits')
+
+ We specifically permit and encourage the inclusion of this
+ software, with or without modifications, in commercial products.
+ We disclaim all warranties covering The FreeType Project and
+ assume no liability related to The FreeType Project.
+
+
+ Finally, many people asked us for a preferred form for a
+ credit/disclaimer to use in compliance with this license. We thus
+ encourage you to use the following text:
+
+ """
+ Portions of this software are copyright © The FreeType
+ Project (https://freetype.org). All rights reserved.
+ """
+
+ Please replace with the value from the FreeType version you
+ actually use.
+
+
+Legal Terms
+===========
+
+0. Definitions
+--------------
+
+ Throughout this license, the terms `package', `FreeType Project',
+ and `FreeType archive' refer to the set of files originally
+ distributed by the authors (David Turner, Robert Wilhelm, and
+ Werner Lemberg) as the `FreeType Project', be they named as alpha,
+ beta or final release.
+
+ `You' refers to the licensee, or person using the project, where
+ `using' is a generic term including compiling the project's source
+ code as well as linking it to form a `program' or `executable'.
+ This program is referred to as `a program using the FreeType
+ engine'.
+
+ This license applies to all files distributed in the original
+ FreeType Project, including all source code, binaries and
+ documentation, unless otherwise stated in the file in its
+ original, unmodified form as distributed in the original archive.
+ If you are unsure whether or not a particular file is covered by
+ this license, you must contact us to verify this.
+
+ The FreeType Project is copyright (C) 1996-2000 by David Turner,
+ Robert Wilhelm, and Werner Lemberg. All rights reserved except as
+ specified below.
+
+1. No Warranty
+--------------
+
+ THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO
+ USE, OF THE FREETYPE PROJECT.
+
+2. Redistribution
+-----------------
+
+ This license grants a worldwide, royalty-free, perpetual and
+ irrevocable right and license to use, execute, perform, compile,
+ display, copy, create derivative works of, distribute and
+ sublicense the FreeType Project (in both source and object code
+ forms) and derivative works thereof for any purpose; and to
+ authorize others to exercise some or all of the rights granted
+ herein, subject to the following conditions:
+
+ o Redistribution of source code must retain this license file
+ (`FTL.TXT') unaltered; any additions, deletions or changes to
+ the original files must be clearly indicated in accompanying
+ documentation. The copyright notices of the unaltered,
+ original files must be preserved in all copies of source
+ files.
+
+ o Redistribution in binary form must provide a disclaimer that
+ states that the software is based in part of the work of the
+ FreeType Team, in the distribution documentation. We also
+ encourage you to put an URL to the FreeType web page in your
+ documentation, though this isn't mandatory.
+
+ These conditions apply to any software derived from or based on
+ the FreeType Project, not just the unmodified files. If you use
+ our work, you must acknowledge us. However, no fee need be paid
+ to us.
+
+3. Advertising
+--------------
+
+ Neither the FreeType authors and contributors nor you shall use
+ the name of the other for commercial, advertising, or promotional
+ purposes without specific prior written permission.
+
+ We suggest, but do not require, that you use one or more of the
+ following phrases to refer to this software in your documentation
+ or advertising materials: `FreeType Project', `FreeType Engine',
+ `FreeType library', or `FreeType Distribution'.
+
+ As you have not signed this license, you are not required to
+ accept it. However, as the FreeType Project is copyrighted
+ material, only this license, or another one contracted with the
+ authors, grants you the right to use, distribute, and modify it.
+ Therefore, by using, distributing, or modifying the FreeType
+ Project, you indicate that you understand and accept all the terms
+ of this license.
+
+4. Contacts
+-----------
+
+ There are two mailing lists related to FreeType:
+
+ o freetype@nongnu.org
+
+ Discusses general use and applications of FreeType, as well as
+ future and wanted additions to the library and distribution.
+ If you are looking for support, start in this list if you
+ haven't found anything to help you in the documentation.
+
+ o freetype-devel@nongnu.org
+
+ Discusses bugs, as well as engine internals, design issues,
+ specific licenses, porting, etc.
+
+ Our home page can be found at
+
+ https://freetype.org
+
+
+--- end of FTL.TXT ---
+
+
+GPLv2.TXT:
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
+
+
diff --git a/ports/uwp/third_party/licenses/libpng.txt b/ports/uwp/third_party/licenses/libpng.txt
new file mode 100644
index 00000000..1b765ae9
--- /dev/null
+++ b/ports/uwp/third_party/licenses/libpng.txt
@@ -0,0 +1,134 @@
+COPYRIGHT NOTICE, DISCLAIMER, and LICENSE
+=========================================
+
+PNG Reference Library License version 2
+---------------------------------------
+
+ * Copyright (c) 1995-2026 The PNG Reference Library Authors.
+ * Copyright (c) 2018-2026 Cosmin Truta.
+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
+ * Copyright (c) 1996-1997 Andreas Dilger.
+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+The software is supplied "as is", without warranty of any kind,
+express or implied, including, without limitation, the warranties
+of merchantability, fitness for a particular purpose, title, and
+non-infringement. In no event shall the Copyright owners, or
+anyone distributing the software, be liable for any damages or
+other liability, whether in contract, tort or otherwise, arising
+from, out of, or in connection with the software, or the use or
+other dealings in the software, even if advised of the possibility
+of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute
+this software, or portions hereof, for any purpose, without fee,
+subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you
+ must not claim that you wrote the original software. If you
+ use this software in a product, an acknowledgment in the product
+ documentation would be appreciated, but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+
+PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)
+-----------------------------------------------------------------------
+
+libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are
+Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are
+derived from libpng-1.0.6, and are distributed according to the same
+disclaimer and license as libpng-1.0.6 with the following individuals
+added to the list of Contributing Authors:
+
+ Simon-Pierre Cadieux
+ Eric S. Raymond
+ Mans Rullgard
+ Cosmin Truta
+ Gilles Vollant
+ James Yu
+ Mandar Sahastrabuddhe
+ Google Inc.
+ Vadim Barkov
+
+and with the following additions to the disclaimer:
+
+ There is no warranty against interference with your enjoyment of
+ the library or against infringement. There is no warranty that our
+ efforts or the library will fulfill any of your particular purposes
+ or needs. This library is provided with all faults, and the entire
+ risk of satisfactory quality, performance, accuracy, and effort is
+ with the user.
+
+Some files in the "contrib" directory and some configure-generated
+files that are distributed with libpng have other copyright owners, and
+are released under other open source licenses.
+
+libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
+Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from
+libpng-0.96, and are distributed according to the same disclaimer and
+license as libpng-0.96, with the following individuals added to the
+list of Contributing Authors:
+
+ Tom Lane
+ Glenn Randers-Pehrson
+ Willem van Schaik
+
+libpng versions 0.89, June 1996, through 0.96, May 1997, are
+Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,
+and are distributed according to the same disclaimer and license as
+libpng-0.88, with the following individuals added to the list of
+Contributing Authors:
+
+ John Bowler
+ Kevin Bracey
+ Sam Bushell
+ Magnus Holmgren
+ Greg Roelofs
+ Tom Tanner
+
+Some files in the "scripts" directory have other copyright owners,
+but are released under this license.
+
+libpng versions 0.5, May 1995, through 0.88, January 1996, are
+Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+For the purposes of this copyright and license, "Contributing Authors"
+is defined as the following set of individuals:
+
+ Andreas Dilger
+ Dave Martindale
+ Guy Eric Schalnat
+ Paul Schmidt
+ Tim Wegner
+
+The PNG Reference Library is supplied "AS IS". The Contributing
+Authors and Group 42, Inc. disclaim all warranties, expressed or
+implied, including, without limitation, the warranties of
+merchantability and of fitness for any purpose. The Contributing
+Authors and Group 42, Inc. assume no liability for direct, indirect,
+incidental, special, exemplary, or consequential damages, which may
+result from the use of the PNG Reference Library, even if advised of
+the possibility of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+source code, or portions hereof, for any purpose, without fee, subject
+to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented.
+
+ 2. Altered versions must be plainly marked as such and must not
+ be misrepresented as being the original source.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+The Contributing Authors and Group 42, Inc. specifically permit,
+without fee, and encourage the use of this source code as a component
+to supporting the PNG file format in commercial products. If you use
+this source code in a product, acknowledgment is not required but would
+be appreciated.
diff --git a/ports/uwp/third_party/licenses/love.txt b/ports/uwp/third_party/licenses/love.txt
new file mode 100644
index 00000000..fdc4af53
--- /dev/null
+++ b/ports/uwp/third_party/licenses/love.txt
@@ -0,0 +1,1355 @@
+Licensing information
+=====================
+
+This distribution contains code from the following projects (full license text below):
+
+ - LOVE
+ Website: https://love2d.org/
+ License: zlib
+ Copyright (c) 2006-2023 LOVE Development Team
+
+ - ENet
+ Website: http://enet.bespin.org/index.html
+ License: MIT/Expat
+ Copyright (c) 2002-2016 Lee Salzman
+
+ - FreeType
+ Website: https://freetype.org/
+ License: FreeType License
+ Copyright (c) 2006-2017 David Turner, Robert Wilhelm, and Werner Lemberg.
+
+ - GLAD
+ Website: http://glad.dav1d.de/
+ License: MIT/Expat
+ Copyright (c) 2013 David Herberth, modified by Sasha Szpakowski
+
+ - glslang
+ Website: https://github.com/KhronosGroup/glslang
+ License: 3-Clause BSD
+ Copyright (C) 2002-2005 3Dlabs Inc. Ltd.
+ Copyright (C) 2013-2016 LunarG, Inc.
+
+ - Kepler Project's lua-compat-5.3
+ Website: https://github.com/keplerproject/lua-compat-5.3
+ License: MIT/Expat
+ Copyright (c) 2015 Kepler Project.
+
+ - lua-enet
+ Website: http://leafo.net/lua-enet/
+ License: MIT/Expat
+ Copyright (C) 2011 by Leaf Corcoran
+
+ - LuaJIT
+ Website: http://luajit.org/
+ License: MIT/Expat
+ LuaJIT is Copyright (c) 2005-2016 Mike Pall
+
+ - Lua's UTF-8 module
+ Website: https://www.lua.org/
+ License: MIT/Expat
+ Copyright (C) 1994-2015 Lua.org, PUC-Rio, 2015 LOVE Development Team.
+
+ - LuaSocket
+ Website: http://w3.impa.br/~diego/software/luasocket/home.html
+ License: MIT/Expat
+ Copyright (C) 2004-2013 Diego Nehab
+
+ - LZ4
+ Website: https://lz4.github.io/lz4/
+ License: 2-Clause BSD
+ Copyright (C) 2011-2015, Yann Collet.
+ You can contact the author at :
+ - LZ4 source repository : https://github.com/Cyan4973/lz4
+ - LZ4 public forum : https://groups.google.com/forum/#!forum/lz4c
+
+ - LodePNG
+ Website: https://lodev.org/lodepng/
+ Source download: https://github.com/lvandeve/lodepng
+ License: zlib
+ Copyright (c) 2005-2020 Lode Vandevenne
+
+ - TinyEXR
+ Website: https://github.com/syoyo/tinyexr
+ License: 3-Clause BSD
+ Copyright (c) 2014 - 2016, Syoyo Fujita
+
+ - UTF8-CPP
+ Website: https://github.com/nemtrif/utfcpp
+ License: Unknown, MIT/Expat-like (listed as UTF8-CPP)
+ Copyright 2006 Nemanja Trifunovic
+
+ - xxHash
+ Website: https://cyan4973.github.io/xxHash/
+ License: 2-Clause BSD
+ Copyright (C) 2012-2016, Yann Collet.
+ You can contact the author at :
+ - xxHash source repository : https://github.com/Cyan4973/xxHash
+
+ - dr_flac
+ Website: https://github.com/mackron/dr_libs
+ Source download: https://github.com/mackron/dr_libs/blob/c5e5355/dr_flac.h
+ License: MIT/Expat
+ Copyright 2018 David Reid
+
+ - stb_image
+ Website: https://github.com/nothings/stb
+ Source download: https://github.com/nothings/stb/blob/e140649ccf40818781b7e408f6228a486f6d254b/stb_image.h
+ License: MIT/Expat
+ Copyright (c) 2017 Sean Barrett
+
+ - libmpg123
+ Website: http://www.mpg123.de/
+ Source download: http://sourceforge.net/projects/mpg123/files/latest/download
+ License: LGPL 2.1
+ Copyright (c) 1995-2013 by Michael Hipp and others, free software under the terms of the LGPL v2.1
+ Detailed information from the debian project:
+ Copyright 1995-2016 by the mpg123 project
+ Copyright 2009-2011 by Malcolm Boczek
+ Copyright 2008 Christian Weisgerber
+ Copyright 2006-2007 by Zuxy Meng
+ Copyright 2000-2002 David Olofson
+ Copyright 1998 Fabrice Bellard
+ Copyright 1997 Mikko Tommila
+
+ - OpenAL Soft
+ Website: https://openal-soft.org/
+ Source download: https://openal-soft.org/#download
+ License: Mixed, licensing information obtained from the debian project
+ - Alc/backends/opensl.c
+ License: Apache 2.0
+ Copyright 2011 The Android Open Source Project
+ - examples/alhrtf.c examples/allatency.c examples/alloopback.c examples/alreverb.c examples/alstream.c examples/altonegen.c examples/common/alhelpers.c examples/common/sdl_sound.c utils/openal-info.c
+ License: MIT/Expat
+ Copyright © 2010, 2015 Chris Robinson
+ - examples/alffplay.c
+ License: unclear, presumed LGPL 2.1 or higher
+ Copyright © 2003 Fabrice Bellard
+ Copyright © Martin Bohme
+ - Alc/bs2b.c OpenAL32/Include/bs2b.h
+ License: MIT/Expat
+ Copyright 2005 by Boris Mikhaylov
+ - cmake/FindALSA.cmake cmake/FindFFmpeg.cmake cmake/FindJACK.cmake cmake/FindSDL2.cmake
+ License: 3-Clause BSD
+ Copyright © 2006 Matthias Kretz
+ Copyright © 2008 Alexander Neundorf
+ Copyright © 2003-2011 Kitware, Inc.
+ Copyright © 2009-2011 Philip Lowman
+ Copyright © 2011 Michael Jansen
+ Copyright © 2012 Benjamin Eikel
+ - utils/makehrtf.c (not included in distribution)
+ License: GPL 2 or higher (2 listed below)
+ Copyright 2011-2014 Christopher Fitzgerald
+ - Everything else:
+ License: LGPL 2.0 or higher (2.1 listed below)
+ Copyright © 1999-2014 the OpenAL team
+ Copyright © 2008-2015 Christopher Fitzgerald
+ Copyright © 2009-2015 Chris Robinson
+ Copyright © 2013 Anis A. Hireche
+ Copyright © 2013 Nasca Octavian Paul
+ Copyright © 2013 Mike Gorchak
+ Copyright © 2014 Timothy Arceri
+
+License text
+============
+
+zlib license
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+ distribution.
+
+MIT/Expat
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+FreeType License
+ The FreeType Project LICENSE
+ ----------------------------
+
+ 2006-Jan-27
+
+ Copyright 1996-2002, 2006 by
+ David Turner, Robert Wilhelm, and Werner Lemberg
+
+ Introduction
+ ============
+
+ The FreeType Project is distributed in several archive packages;
+ some of them may contain, in addition to the FreeType font engine,
+ various tools and contributions which rely on, or relate to, the
+ FreeType Project.
+
+ This license applies to all files found in such packages, and
+ which do not fall under their own explicit license. The license
+ affects thus the FreeType font engine, the test programs,
+ documentation and makefiles, at the very least.
+
+ This license was inspired by the BSD, Artistic, and IJG
+ (Independent JPEG Group) licenses, which all encourage inclusion
+ and use of free software in commercial and freeware products
+ alike. As a consequence, its main points are that:
+
+ o We don't promise that this software works. However, we will be
+ interested in any kind of bug reports. (`as is' distribution)
+
+ o You can use this software for whatever you want, in parts or
+ full form, without having to pay us. (`royalty-free' usage)
+
+ o You may not pretend that you wrote this software. If you use
+ it, or only parts of it, in a program, you must acknowledge
+ somewhere in your documentation that you have used the
+ FreeType code. (`credits')
+
+ We specifically permit and encourage the inclusion of this
+ software, with or without modifications, in commercial products.
+ We disclaim all warranties covering The FreeType Project and
+ assume no liability related to The FreeType Project.
+
+
+ Finally, many people asked us for a preferred form for a
+ credit/disclaimer to use in compliance with this license. We thus
+ encourage you to use the following text:
+
+ """
+ Portions of this software are copyright © The FreeType
+ Project (www.freetype.org). All rights reserved.
+ """
+
+ Please replace with the value from the FreeType version you
+ actually use.
+
+
+ Legal Terms
+ ===========
+
+ 0. Definitions
+ --------------
+
+ Throughout this license, the terms `package', `FreeType Project',
+ and `FreeType archive' refer to the set of files originally
+ distributed by the authors (David Turner, Robert Wilhelm, and
+ Werner Lemberg) as the `FreeType Project', be they named as alpha,
+ beta or final release.
+
+ `You' refers to the licensee, or person using the project, where
+ `using' is a generic term including compiling the project's source
+ code as well as linking it to form a `program' or `executable'.
+ This program is referred to as `a program using the FreeType
+ engine'.
+
+ This license applies to all files distributed in the original
+ FreeType Project, including all source code, binaries and
+ documentation, unless otherwise stated in the file in its
+ original, unmodified form as distributed in the original archive.
+ If you are unsure whether or not a particular file is covered by
+ this license, you must contact us to verify this.
+
+ The FreeType Project is copyright (C) 1996-2000 by David Turner,
+ Robert Wilhelm, and Werner Lemberg. All rights reserved except as
+ specified below.
+
+ 1. No Warranty
+ --------------
+
+ THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO
+ USE, OF THE FREETYPE PROJECT.
+
+ 2. Redistribution
+ -----------------
+
+ This license grants a worldwide, royalty-free, perpetual and
+ irrevocable right and license to use, execute, perform, compile,
+ display, copy, create derivative works of, distribute and
+ sublicense the FreeType Project (in both source and object code
+ forms) and derivative works thereof for any purpose; and to
+ authorize others to exercise some or all of the rights granted
+ herein, subject to the following conditions:
+
+ o Redistribution of source code must retain this license file
+ (`FTL.TXT') unaltered; any additions, deletions or changes to
+ the original files must be clearly indicated in accompanying
+ documentation. The copyright notices of the unaltered,
+ original files must be preserved in all copies of source
+ files.
+
+ o Redistribution in binary form must provide a disclaimer that
+ states that the software is based in part of the work of the
+ FreeType Team, in the distribution documentation. We also
+ encourage you to put an URL to the FreeType web page in your
+ documentation, though this isn't mandatory.
+
+ These conditions apply to any software derived from or based on
+ the FreeType Project, not just the unmodified files. If you use
+ our work, you must acknowledge us. However, no fee need be paid
+ to us.
+
+ 3. Advertising
+ --------------
+
+ Neither the FreeType authors and contributors nor you shall use
+ the name of the other for commercial, advertising, or promotional
+ purposes without specific prior written permission.
+
+ We suggest, but do not require, that you use one or more of the
+ following phrases to refer to this software in your documentation
+ or advertising materials: `FreeType Project', `FreeType Engine',
+ `FreeType library', or `FreeType Distribution'.
+
+ As you have not signed this license, you are not required to
+ accept it. However, as the FreeType Project is copyrighted
+ material, only this license, or another one contracted with the
+ authors, grants you the right to use, distribute, and modify it.
+ Therefore, by using, distributing, or modifying the FreeType
+ Project, you indicate that you understand and accept all the terms
+ of this license.
+
+ 4. Contacts
+ -----------
+
+ There are two mailing lists related to FreeType:
+
+ o freetype@nongnu.org
+
+ Discusses general use and applications of FreeType, as well as
+ future and wanted additions to the library and distribution.
+ If you are looking for support, start in this list if you
+ haven't found anything to help you in the documentation.
+
+ o freetype-devel@nongnu.org
+
+ Discusses bugs, as well as engine internals, design issues,
+ specific licenses, porting, etc.
+
+ Our home page can be found at
+
+ http://www.freetype.org
+
+3-Clause BSD
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ Neither the name of 3Dlabs Inc. Ltd. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+2-Clause BSD
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+UTF8-CPP
+ Permission is hereby granted, free of charge, to any person or organization
+ obtaining a copy of the software and accompanying documentation covered by
+ this license (the "Software") to use, reproduce, display, distribute,
+ execute, and transmit the Software, and to prepare derivative works of the
+ Software, and to permit third-parties to whom the Software is furnished to
+ do so, all subject to the following:
+
+ The copyright notices in the Software and this entire statement, including
+ the above license grant, this restriction and the following disclaimer,
+ must be included in all copies of the Software, in whole or in part, and
+ all derivative works of the Software, unless such copies or derivative
+ works are solely in the form of machine-executable object code generated by
+ a source language processor.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+LGPL 2.1
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ [This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+ freedom to share and change it. By contrast, the GNU General Public
+ Licenses are intended to guarantee your freedom to share and change
+ free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+ specially designated software packages--typically libraries--of the
+ Free Software Foundation and other authors who decide to use it. You
+ can use it too, but we suggest you first think carefully about whether
+ this license or the ordinary General Public License is the better
+ strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+ not price. Our General Public Licenses are designed to make sure that
+ you have the freedom to distribute copies of free software (and charge
+ for this service if you wish); that you receive source code or can get
+ it if you want it; that you can change the software and use pieces of
+ it in new free programs; and that you are informed that you can do
+ these things.
+
+ To protect your rights, we need to make restrictions that forbid
+ distributors to deny you these rights or to ask you to surrender these
+ rights. These restrictions translate to certain responsibilities for
+ you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+ or for a fee, you must give the recipients all the rights that we gave
+ you. You must make sure that they, too, receive or can get the source
+ code. If you link other code with the library, you must provide
+ complete object files to the recipients, so that they can relink them
+ with the library after making changes to the library and recompiling
+ it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+ library, and (2) we offer you this license, which gives you legal
+ permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+ there is no warranty for the free library. Also, if the library is
+ modified by someone else and passed on, the recipients should know
+ that what they have is not the original version, so that the original
+ author's reputation will not be affected by problems that might be
+ introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+ any free program. We wish to make sure that a company cannot
+ effectively restrict the users of a free program by obtaining a
+ restrictive license from a patent holder. Therefore, we insist that
+ any patent license obtained for a version of the library must be
+ consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ ordinary GNU General Public License. This license, the GNU Lesser
+ General Public License, applies to certain designated libraries, and
+ is quite different from the ordinary General Public License. We use
+ this license for certain libraries in order to permit linking those
+ libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+ a shared library, the combination of the two is legally speaking a
+ combined work, a derivative of the original library. The ordinary
+ General Public License therefore permits such linking only if the
+ entire combination fits its criteria of freedom. The Lesser General
+ Public License permits more lax criteria for linking other code with
+ the library.
+
+ We call this license the "Lesser" General Public License because it
+ does Less to protect the user's freedom than the ordinary General
+ Public License. It also provides other free software developers Less
+ of an advantage over competing non-free programs. These disadvantages
+ are the reason we use the ordinary General Public License for many
+ libraries. However, the Lesser license provides advantages in certain
+ special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+ encourage the widest possible use of a certain library, so that it becomes
+ a de-facto standard. To achieve this, non-free programs must be
+ allowed to use the library. A more frequent case is that a free
+ library does the same job as widely used non-free libraries. In this
+ case, there is little to gain by limiting the free library to free
+ software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+ programs enables a greater number of people to use a large body of
+ free software. For example, permission to use the GNU C Library in
+ non-free programs enables many more people to use the whole GNU
+ operating system, as well as its variant, the GNU/Linux operating
+ system.
+
+ Although the Lesser General Public License is Less protective of the
+ users' freedom, it does ensure that the user of a program that is
+ linked with the Library has the freedom and the wherewithal to run
+ that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+ modification follow. Pay close attention to the difference between a
+ "work based on the library" and a "work that uses the library". The
+ former contains code derived from the library, whereas the latter must
+ be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+ program which contains a notice placed by the copyright holder or
+ other authorized party saying it may be distributed under the terms of
+ this Lesser General Public License (also called "this License").
+ Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+ prepared so as to be conveniently linked with application programs
+ (which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+ which has been distributed under these terms. A "work based on the
+ Library" means either the Library or any derivative work under
+ copyright law: that is to say, a work containing the Library or a
+ portion of it, either verbatim or with modifications and/or translated
+ straightforwardly into another language. (Hereinafter, translation is
+ included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+ making modifications to it. For a library, complete source code means
+ all the source code for all modules it contains, plus any associated
+ interface definition files, plus the scripts used to control compilation
+ and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+ covered by this License; they are outside its scope. The act of
+ running a program using the Library is not restricted, and output from
+ such a program is covered only if its contents constitute a work based
+ on the Library (independent of the use of the Library in a tool for
+ writing it). Whether that is true depends on what the Library does
+ and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+ complete source code as you receive it, in any medium, provided that
+ you conspicuously and appropriately publish on each copy an
+ appropriate copyright notice and disclaimer of warranty; keep intact
+ all the notices that refer to this License and to the absence of any
+ warranty; and distribute a copy of this License along with the
+ Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+ and you may at your option offer warranty protection in exchange for a
+ fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+ of it, thus forming a work based on the Library, and copy and
+ distribute such modifications or work under the terms of Section 1
+ above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+ These requirements apply to the modified work as a whole. If
+ identifiable sections of that work are not derived from the Library,
+ and can be reasonably considered independent and separate works in
+ themselves, then this License, and its terms, do not apply to those
+ sections when you distribute them as separate works. But when you
+ distribute the same sections as part of a whole which is a work based
+ on the Library, the distribution of the whole must be on the terms of
+ this License, whose permissions for other licensees extend to the
+ entire whole, and thus to each and every part regardless of who wrote
+ it.
+
+ Thus, it is not the intent of this section to claim rights or contest
+ your rights to work written entirely by you; rather, the intent is to
+ exercise the right to control the distribution of derivative or
+ collective works based on the Library.
+
+ In addition, mere aggregation of another work not based on the Library
+ with the Library (or with a work based on the Library) on a volume of
+ a storage or distribution medium does not bring the other work under
+ the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+ License instead of this License to a given copy of the Library. To do
+ this, you must alter all the notices that refer to this License, so
+ that they refer to the ordinary GNU General Public License, version 2,
+ instead of to this License. (If a newer version than version 2 of the
+ ordinary GNU General Public License has appeared, then you can specify
+ that version instead if you wish.) Do not make any other change in
+ these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+ that copy, so the ordinary GNU General Public License applies to all
+ subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+ the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+ derivative of it, under Section 2) in object code or executable form
+ under the terms of Sections 1 and 2 above provided that you accompany
+ it with the complete corresponding machine-readable source code, which
+ must be distributed under the terms of Sections 1 and 2 above on a
+ medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+ from a designated place, then offering equivalent access to copy the
+ source code from the same place satisfies the requirement to
+ distribute the source code, even though third parties are not
+ compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+ Library, but is designed to work with the Library by being compiled or
+ linked with it, is called a "work that uses the Library". Such a
+ work, in isolation, is not a derivative work of the Library, and
+ therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+ creates an executable that is a derivative of the Library (because it
+ contains portions of the Library), rather than a "work that uses the
+ library". The executable is therefore covered by this License.
+ Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+ that is part of the Library, the object code for the work may be a
+ derivative work of the Library even though the source code is not.
+ Whether this is true is especially significant if the work can be
+ linked without the Library, or if the work is itself a library. The
+ threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+ structure layouts and accessors, and small macros and small inline
+ functions (ten lines or less in length), then the use of the object
+ file is unrestricted, regardless of whether it is legally a derivative
+ work. (Executables containing this object code plus portions of the
+ Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+ distribute the object code for the work under the terms of Section 6.
+ Any executables containing that work also fall under Section 6,
+ whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+ link a "work that uses the Library" with the Library to produce a
+ work containing portions of the Library, and distribute that work
+ under terms of your choice, provided that the terms permit
+ modification of the work for the customer's own use and reverse
+ engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+ Library is used in it and that the Library and its use are covered by
+ this License. You must supply a copy of this License. If the work
+ during execution displays copyright notices, you must include the
+ copyright notice for the Library among them, as well as a reference
+ directing the user to the copy of this License. Also, you must do one
+ of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+ Library" must include any data and utility programs needed for
+ reproducing the executable from it. However, as a special exception,
+ the materials to be distributed need not include anything that is
+ normally distributed (in either source or binary form) with the major
+ components (compiler, kernel, and so on) of the operating system on
+ which the executable runs, unless that component itself accompanies
+ the executable.
+
+ It may happen that this requirement contradicts the license
+ restrictions of other proprietary libraries that do not normally
+ accompany the operating system. Such a contradiction means you cannot
+ use both them and the Library together in an executable that you
+ distribute.
+
+ 7. You may place library facilities that are a work based on the
+ Library side-by-side in a single library together with other library
+ facilities not covered by this License, and distribute such a combined
+ library, provided that the separate distribution of the work based on
+ the Library and of the other library facilities is otherwise
+ permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+ the Library except as expressly provided under this License. Any
+ attempt otherwise to copy, modify, sublicense, link with, or
+ distribute the Library is void, and will automatically terminate your
+ rights under this License. However, parties who have received copies,
+ or rights, from you under this License will not have their licenses
+ terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+ signed it. However, nothing else grants you permission to modify or
+ distribute the Library or its derivative works. These actions are
+ prohibited by law if you do not accept this License. Therefore, by
+ modifying or distributing the Library (or any work based on the
+ Library), you indicate your acceptance of this License to do so, and
+ all its terms and conditions for copying, distributing or modifying
+ the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+ Library), the recipient automatically receives a license from the
+ original licensor to copy, distribute, link with or modify the Library
+ subject to these terms and conditions. You may not impose any further
+ restrictions on the recipients' exercise of the rights granted herein.
+ You are not responsible for enforcing compliance by third parties with
+ this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+ infringement or for any other reason (not limited to patent issues),
+ conditions are imposed on you (whether by court order, agreement or
+ otherwise) that contradict the conditions of this License, they do not
+ excuse you from the conditions of this License. If you cannot
+ distribute so as to satisfy simultaneously your obligations under this
+ License and any other pertinent obligations, then as a consequence you
+ may not distribute the Library at all. For example, if a patent
+ license would not permit royalty-free redistribution of the Library by
+ all those who receive copies directly or indirectly through you, then
+ the only way you could satisfy both it and this License would be to
+ refrain entirely from distribution of the Library.
+
+ If any portion of this section is held invalid or unenforceable under any
+ particular circumstance, the balance of the section is intended to apply,
+ and the section as a whole is intended to apply in other circumstances.
+
+ It is not the purpose of this section to induce you to infringe any
+ patents or other property right claims or to contest validity of any
+ such claims; this section has the sole purpose of protecting the
+ integrity of the free software distribution system which is
+ implemented by public license practices. Many people have made
+ generous contributions to the wide range of software distributed
+ through that system in reliance on consistent application of that
+ system; it is up to the author/donor to decide if he or she is willing
+ to distribute software through any other system and a licensee cannot
+ impose that choice.
+
+ This section is intended to make thoroughly clear what is believed to
+ be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+ certain countries either by patents or by copyrighted interfaces, the
+ original copyright holder who places the Library under this License may add
+ an explicit geographical distribution limitation excluding those countries,
+ so that distribution is permitted only in or among countries not thus
+ excluded. In such case, this License incorporates the limitation as if
+ written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+ versions of the Lesser General Public License from time to time.
+ Such new versions will be similar in spirit to the present version,
+ but may differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the Library
+ specifies a version number of this License which applies to it and
+ "any later version", you have the option of following the terms and
+ conditions either of that version or of any later version published by
+ the Free Software Foundation. If the Library does not specify a
+ license version number, you may choose any version ever published by
+ the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+ programs whose distribution conditions are incompatible with these,
+ write to the author to ask for permission. For software which is
+ copyrighted by the Free Software Foundation, write to the Free
+ Software Foundation; we sometimes make exceptions for this. Our
+ decision will be guided by the two goals of preserving the free status
+ of all derivatives of our free software and of promoting the sharing
+ and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+ WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+ EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+ OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+ KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+ LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+ THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+ WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+ AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+ FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+ CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+ LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+ RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+ FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+ SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+ possible use to the public, we recommend making it free software that
+ everyone can redistribute and change. You can do so by permitting
+ redistribution under these terms (or, alternatively, under the terms of the
+ ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+ safest to attach them to the start of each source file to most effectively
+ convey the exclusion of warranty; and each file should have at least the
+ "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+ Also add information on how to contact you by electronic and paper mail.
+
+ You should also get your employer (if you work as a programmer) or your
+ school, if any, to sign a "copyright disclaimer" for the library, if
+ necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+ That's all there is to it!
+
+GPL 2
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+ freedom to share and change it. By contrast, the GNU General Public
+ License is intended to guarantee your freedom to share and change free
+ software--to make sure the software is free for all its users. This
+ General Public License applies to most of the Free Software
+ Foundation's software and to any other program whose authors commit to
+ using it. (Some other Free Software Foundation software is covered by
+ the GNU Lesser General Public License instead.) You can apply it to
+ your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+ price. Our General Public Licenses are designed to make sure that you
+ have the freedom to distribute copies of free software (and charge for
+ this service if you wish), that you receive source code or can get it
+ if you want it, that you can change the software or use pieces of it
+ in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+ anyone to deny you these rights or to ask you to surrender the rights.
+ These restrictions translate to certain responsibilities for you if you
+ distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+ gratis or for a fee, you must give the recipients all the rights that
+ you have. You must make sure that they, too, receive or can get the
+ source code. And you must show them these terms so they know their
+ rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+ (2) offer you this license which gives you legal permission to copy,
+ distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+ that everyone understands that there is no warranty for this free
+ software. If the software is modified by someone else and passed on, we
+ want its recipients to know that what they have is not the original, so
+ that any problems introduced by others will not reflect on the original
+ authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+ patents. We wish to avoid the danger that redistributors of a free
+ program will individually obtain patent licenses, in effect making the
+ program proprietary. To prevent this, we have made it clear that any
+ patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+ modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+ a notice placed by the copyright holder saying it may be distributed
+ under the terms of this General Public License. The "Program", below,
+ refers to any such program or work, and a "work based on the Program"
+ means either the Program or any derivative work under copyright law:
+ that is to say, a work containing the Program or a portion of it,
+ either verbatim or with modifications and/or translated into another
+ language. (Hereinafter, translation is included without limitation in
+ the term "modification".) Each licensee is addressed as "you".
+
+ Activities other than copying, distribution and modification are not
+ covered by this License; they are outside its scope. The act of
+ running the Program is not restricted, and the output from the Program
+ is covered only if its contents constitute a work based on the
+ Program (independent of having been made by running the Program).
+ Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+ source code as you receive it, in any medium, provided that you
+ conspicuously and appropriately publish on each copy an appropriate
+ copyright notice and disclaimer of warranty; keep intact all the
+ notices that refer to this License and to the absence of any warranty;
+ and give any other recipients of the Program a copy of this License
+ along with the Program.
+
+ You may charge a fee for the physical act of transferring a copy, and
+ you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+ of it, thus forming a work based on the Program, and copy and
+ distribute such modifications or work under the terms of Section 1
+ above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+ These requirements apply to the modified work as a whole. If
+ identifiable sections of that work are not derived from the Program,
+ and can be reasonably considered independent and separate works in
+ themselves, then this License, and its terms, do not apply to those
+ sections when you distribute them as separate works. But when you
+ distribute the same sections as part of a whole which is a work based
+ on the Program, the distribution of the whole must be on the terms of
+ this License, whose permissions for other licensees extend to the
+ entire whole, and thus to each and every part regardless of who wrote it.
+
+ Thus, it is not the intent of this section to claim rights or contest
+ your rights to work written entirely by you; rather, the intent is to
+ exercise the right to control the distribution of derivative or
+ collective works based on the Program.
+
+ In addition, mere aggregation of another work not based on the Program
+ with the Program (or with a work based on the Program) on a volume of
+ a storage or distribution medium does not bring the other work under
+ the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+ under Section 2) in object code or executable form under the terms of
+ Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+ The source code for a work means the preferred form of the work for
+ making modifications to it. For an executable work, complete source
+ code means all the source code for all modules it contains, plus any
+ associated interface definition files, plus the scripts used to
+ control compilation and installation of the executable. However, as a
+ special exception, the source code distributed need not include
+ anything that is normally distributed (in either source or binary
+ form) with the major components (compiler, kernel, and so on) of the
+ operating system on which the executable runs, unless that component
+ itself accompanies the executable.
+
+ If distribution of executable or object code is made by offering
+ access to copy from a designated place, then offering equivalent
+ access to copy the source code from the same place counts as
+ distribution of the source code, even though third parties are not
+ compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+ except as expressly provided under this License. Any attempt
+ otherwise to copy, modify, sublicense or distribute the Program is
+ void, and will automatically terminate your rights under this License.
+ However, parties who have received copies, or rights, from you under
+ this License will not have their licenses terminated so long as such
+ parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+ signed it. However, nothing else grants you permission to modify or
+ distribute the Program or its derivative works. These actions are
+ prohibited by law if you do not accept this License. Therefore, by
+ modifying or distributing the Program (or any work based on the
+ Program), you indicate your acceptance of this License to do so, and
+ all its terms and conditions for copying, distributing or modifying
+ the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+ Program), the recipient automatically receives a license from the
+ original licensor to copy, distribute or modify the Program subject to
+ these terms and conditions. You may not impose any further
+ restrictions on the recipients' exercise of the rights granted herein.
+ You are not responsible for enforcing compliance by third parties to
+ this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+ infringement or for any other reason (not limited to patent issues),
+ conditions are imposed on you (whether by court order, agreement or
+ otherwise) that contradict the conditions of this License, they do not
+ excuse you from the conditions of this License. If you cannot
+ distribute so as to satisfy simultaneously your obligations under this
+ License and any other pertinent obligations, then as a consequence you
+ may not distribute the Program at all. For example, if a patent
+ license would not permit royalty-free redistribution of the Program by
+ all those who receive copies directly or indirectly through you, then
+ the only way you could satisfy both it and this License would be to
+ refrain entirely from distribution of the Program.
+
+ If any portion of this section is held invalid or unenforceable under
+ any particular circumstance, the balance of the section is intended to
+ apply and the section as a whole is intended to apply in other
+ circumstances.
+
+ It is not the purpose of this section to induce you to infringe any
+ patents or other property right claims or to contest validity of any
+ such claims; this section has the sole purpose of protecting the
+ integrity of the free software distribution system, which is
+ implemented by public license practices. Many people have made
+ generous contributions to the wide range of software distributed
+ through that system in reliance on consistent application of that
+ system; it is up to the author/donor to decide if he or she is willing
+ to distribute software through any other system and a licensee cannot
+ impose that choice.
+
+ This section is intended to make thoroughly clear what is believed to
+ be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+ certain countries either by patents or by copyrighted interfaces, the
+ original copyright holder who places the Program under this License
+ may add an explicit geographical distribution limitation excluding
+ those countries, so that distribution is permitted only in or among
+ countries not thus excluded. In such case, this License incorporates
+ the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+ of the General Public License from time to time. Such new versions will
+ be similar in spirit to the present version, but may differ in detail to
+ address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the Program
+ specifies a version number of this License which applies to it and "any
+ later version", you have the option of following the terms and conditions
+ either of that version or of any later version published by the Free
+ Software Foundation. If the Program does not specify a version number of
+ this License, you may choose any version ever published by the Free Software
+ Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+ programs whose distribution conditions are different, write to the author
+ to ask for permission. For software which is copyrighted by the Free
+ Software Foundation, write to the Free Software Foundation; we sometimes
+ make exceptions for this. Our decision will be guided by the two goals
+ of preserving the free status of all derivatives of our free software and
+ of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+ FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+ OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+ TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+ PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+ REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+ INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+ OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+ possible use to the public, the best way to achieve this is to make it
+ free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+ to attach them to the start of each source file to most effectively
+ convey the exclusion of warranty; and each file should have at least
+ the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+ Also add information on how to contact you by electronic and paper mail.
+
+ If the program is interactive, make it output a short notice like this
+ when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+ The hypothetical commands `show w' and `show c' should show the appropriate
+ parts of the General Public License. Of course, the commands you use may
+ be called something other than `show w' and `show c'; they could even be
+ mouse-clicks or menu items--whatever suits your program.
+
+ You should also get your employer (if you work as a programmer) or your
+ school, if any, to sign a "copyright disclaimer" for the program, if
+ necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+ This General Public License does not permit incorporating your program into
+ proprietary programs. If your program is a subroutine library, you may
+ consider it more useful to permit linking proprietary applications with the
+ library. If this is what you want to do, use the GNU Lesser General
+ Public License instead of this License.
+
+Apache 2.0
+ Apache License
+
+ Version 2.0, January 2004
+
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+ You must give any other recipients of the Work or Derivative Works a copy of this License; and
+ You must cause any modified files to carry prominent notices stating that You changed the files; and
+ You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+ If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+ APPENDIX: How to apply the Apache License to your work
+
+ To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/ports/uwp/third_party/licenses/luajit.txt b/ports/uwp/third_party/licenses/luajit.txt
new file mode 100644
index 00000000..955d0d1e
--- /dev/null
+++ b/ports/uwp/third_party/licenses/luajit.txt
@@ -0,0 +1,56 @@
+===============================================================================
+LuaJIT -- a Just-In-Time Compiler for Lua. https://luajit.org/
+
+Copyright (C) 2005-2026 Mike Pall. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+[ MIT license: https://www.opensource.org/licenses/mit-license.php ]
+
+===============================================================================
+[ LuaJIT includes code from Lua 5.1/5.2, which has this license statement: ]
+
+Copyright (C) 1994-2012 Lua.org, PUC-Rio.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+===============================================================================
+[ LuaJIT includes code from dlmalloc, which has this license statement: ]
+
+This is a version (aka dlmalloc) of malloc/free/realloc written by
+Doug Lea and released to the public domain, as explained at
+https://creativecommons.org/licenses/publicdomain
+
+===============================================================================
diff --git a/ports/uwp/third_party/licenses/ogg.txt b/ports/uwp/third_party/licenses/ogg.txt
new file mode 100644
index 00000000..6111c6c5
--- /dev/null
+++ b/ports/uwp/third_party/licenses/ogg.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2002, Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/ports/uwp/third_party/licenses/openal-soft.txt b/ports/uwp/third_party/licenses/openal-soft.txt
new file mode 100644
index 00000000..c8152d0d
--- /dev/null
+++ b/ports/uwp/third_party/licenses/openal-soft.txt
@@ -0,0 +1,501 @@
+COPYING:
+
+ GNU LIBRARY GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL. It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it. You can use it for
+your libraries, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library. If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software. To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+ Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs. This
+license, the GNU Library General Public License, applies to certain
+designated libraries. This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+ The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it. Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program. However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+ Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries. We
+concluded that weaker conditions might promote sharing better.
+
+ However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves. This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them. (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.) The hope is that this
+will lead to faster development of free libraries.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+ Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+
+ GNU LIBRARY GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License"). Each licensee is
+addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ c) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ d) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Library General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+
+pffft Notice:
+
+//$ nobt
+
+/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com )
+ * Copyright (c) 2023 Christopher Robinson
+ *
+ * Based on original fortran 77 code from FFTPACKv4 from NETLIB
+ * (http://www.netlib.org/fftpack), authored by Dr Paul Swarztrauber
+ * of NCAR, in 1985.
+ *
+ * As confirmed by the NCAR fftpack software curators, the following
+ * FFTPACKv5 license applies to FFTPACKv4 sources. My changes are
+ * released under the same terms.
+ *
+ * FFTPACK license:
+ *
+ * http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html
+ *
+ * Copyright (c) 2004 the University Corporation for Atmospheric
+ * Research ("UCAR"). All rights reserved. Developed by NCAR's
+ * Computational and Information Systems Laboratory, UCAR,
+ * www.cisl.ucar.edu.
+ *
+ * Redistribution and use of the Software in source and binary forms,
+ * with or without modification, is permitted provided that the
+ * following conditions are met:
+ *
+ * - Neither the names of NCAR's Computational and Information Systems
+ * Laboratory, the University Corporation for Atmospheric Research,
+ * nor the names of its sponsors or contributors may be used to
+ * endorse or promote products derived from this Software without
+ * specific prior written permission.
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notices, this list of conditions, and the disclaimer below.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions, and the disclaimer below in the
+ * documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+ * SOFTWARE.
+ *
+ *
+ * PFFFT : a Pretty Fast FFT.
+ *
+ * This file is largerly based on the original FFTPACK implementation, modified
+ * in order to take advantage of SIMD instructions of modern CPUs.
+ */
+
+
diff --git a/ports/uwp/third_party/licenses/sdl2.txt b/ports/uwp/third_party/licenses/sdl2.txt
new file mode 100644
index 00000000..23abb73f
--- /dev/null
+++ b/ports/uwp/third_party/licenses/sdl2.txt
@@ -0,0 +1,18 @@
+Copyright (C) 1997-2025 Sam Lantinga
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
diff --git a/ports/uwp/third_party/licenses/theora.txt b/ports/uwp/third_party/licenses/theora.txt
new file mode 100644
index 00000000..b90629a2
--- /dev/null
+++ b/ports/uwp/third_party/licenses/theora.txt
@@ -0,0 +1,54 @@
+COPYING:
+
+Copyright (C) 2002-2009 Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+LICENSE:
+
+Please see the file COPYING for the copyright license for this software.
+
+In addition to and irrespective of the copyright license associated
+with this software, On2 Technologies, Inc. makes the following statement
+regarding technology used in this software:
+
+ On2 represents and warrants that it shall not assert any rights
+ relating to infringement of On2's registered patents, nor initiate
+ any litigation asserting such rights, against any person who, or
+ entity which utilizes the On2 VP3 Codec Software, including any
+ use, distribution, and sale of said Software; which make changes,
+ modifications, and improvements in said Software; and to use,
+ distribute, and sell said changes as well as applications for other
+ fields of use.
+
+This reference implementation is originally derived from the On2 VP3
+Codec Software, and the Theora video format is essentially compatible
+with the VP3 video format, consisting of a backward-compatible superset.
+
+
diff --git a/ports/uwp/third_party/licenses/vorbis.txt b/ports/uwp/third_party/licenses/vorbis.txt
new file mode 100644
index 00000000..fb456a87
--- /dev/null
+++ b/ports/uwp/third_party/licenses/vorbis.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2002-2020 Xiph.org Foundation
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of the Xiph.org Foundation nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/ports/uwp/third_party/licenses/zlib.txt b/ports/uwp/third_party/licenses/zlib.txt
new file mode 100644
index 00000000..b7a69d05
--- /dev/null
+++ b/ports/uwp/third_party/licenses/zlib.txt
@@ -0,0 +1,22 @@
+Copyright notice:
+
+ (C) 1995-2026 Jean-loup Gailly and Mark Adler
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+ Jean-loup Gailly Mark Adler
+ jloup@gzip.org madler@alumni.caltech.edu
diff --git a/ports/uwp/third_party/love/README.md b/ports/uwp/third_party/love/README.md
new file mode 100644
index 00000000..cca8b2fa
--- /dev/null
+++ b/ports/uwp/third_party/love/README.md
@@ -0,0 +1,7 @@
+# LÖVE UWP binaries
+
+These x64 UWP Release binaries were built from [`caorthann-celt/love-xbox-uwp`](https://github.com/caorthann-celt/love-xbox-uwp) at commit `cdf85f28ed794c95d6f5f7e3a8f23ca9ee1fbdcb`.
+
+The build uses LÖVE 11.5, LuaJIT, SDL2, and ANGLE. Keep the DLLs and import libraries together; they are one binary interface.
+
+The normal package build consumes these files directly. Rebuilding the backend is a separate dependency maintenance task.
diff --git a/ports/uwp/third_party/love/bin/love.dll b/ports/uwp/third_party/love/bin/love.dll
new file mode 100644
index 00000000..306fc5c5
Binary files /dev/null and b/ports/uwp/third_party/love/bin/love.dll differ
diff --git a/ports/uwp/third_party/love/bin/lua51.dll b/ports/uwp/third_party/love/bin/lua51.dll
new file mode 100644
index 00000000..6ee38a88
Binary files /dev/null and b/ports/uwp/third_party/love/bin/lua51.dll differ
diff --git a/ports/uwp/third_party/love/lib/liblove.lib b/ports/uwp/third_party/love/lib/liblove.lib
new file mode 100644
index 00000000..ff75a98d
Binary files /dev/null and b/ports/uwp/third_party/love/lib/liblove.lib differ
diff --git a/ports/uwp/third_party/love/lib/lovestatic.lib b/ports/uwp/third_party/love/lib/lovestatic.lib
new file mode 100644
index 00000000..af2d8ad1
Binary files /dev/null and b/ports/uwp/third_party/love/lib/lovestatic.lib differ
diff --git a/ports/uwp/third_party/love/lib/lua51.lib b/ports/uwp/third_party/love/lib/lua51.lib
new file mode 100644
index 00000000..1ad14ea0
Binary files /dev/null and b/ports/uwp/third_party/love/lib/lua51.lib differ
diff --git a/ports/uwp/third_party/manifest.json b/ports/uwp/third_party/manifest.json
new file mode 100644
index 00000000..ec790a24
--- /dev/null
+++ b/ports/uwp/third_party/manifest.json
@@ -0,0 +1,497 @@
+{
+ "schemaVersion": 1,
+ "platform": "uwp",
+ "architecture": "x64",
+ "configuration": "Release",
+ "sources": {
+ "love": {
+ "version": "11.5",
+ "repository": "https://github.com/caorthann-celt/love-xbox-uwp.git",
+ "commit": "cdf85f28ed794c95d6f5f7e3a8f23ca9ee1fbdcb"
+ },
+ "luajit": {
+ "repository": "https://github.com/LuaJIT/LuaJIT.git",
+ "commit": "4886b676a698acc4bbdf54adfabb3e33a8c020e8"
+ },
+ "sdl2": {
+ "version": "2.32.10",
+ "repository": "https://github.com/libsdl-org/SDL.git",
+ "ref": "release-2.32.10",
+ "commit": "5d249570393f7a37e037abf22cd6012a4cc56a71",
+ "revision": "release-2.32.10-xbox-uwp",
+ "patch": "sdl2/patches/xbox-wgi-controller.patch"
+ },
+ "angle": {
+ "repository": "https://github.com/SternXD/angle.git",
+ "commit": "45b0b1e03400b7a10aaa9a077e196d1abcddafce",
+ "gnArgs": [
+ "target_os = \"winuwp\"",
+ "target_cpu = \"x64\"",
+ "is_debug = false",
+ "is_component_build = false",
+ "is_clang = false",
+ "use_custom_libcxx = false",
+ "use_custom_libcxx_for_host = false",
+ "angle_enable_vulkan = false",
+ "angle_enable_gl = false",
+ "angle_enable_d3d9 = false",
+ "angle_enable_d3d11 = true",
+ "angle_build_tests = false",
+ "treat_warnings_as_errors = false"
+ ]
+ },
+ "depotTools": {
+ "repository": "https://chromium.googlesource.com/chromium/tools/depot_tools.git",
+ "commit": "edcece7fb3d5e266a0f60ceb77fb37e94bfff3ca"
+ },
+ "vcpkg": {
+ "repository": "https://github.com/microsoft/vcpkg.git",
+ "commit": "80f9bcfa455e875d9c1bf7a7c6692d7e1e481061",
+ "packages": [
+ "freetype[brotli,bzip2,png,zlib]",
+ "libogg",
+ "libtheora",
+ "libvorbis",
+ "openal-soft",
+ "zlib"
+ ],
+ "runtimeFiles": [
+ "brotlicommon.dll",
+ "brotlidec.dll",
+ "bz2.dll",
+ "fmt.dll",
+ "freetype.dll",
+ "libpng16.dll",
+ "ogg.dll",
+ "OpenAL32.dll",
+ "theora.dll",
+ "theoradec.dll",
+ "vorbis.dll",
+ "vorbisfile.dll",
+ "z.dll"
+ ],
+ "licenses": {
+ "brotli": "brotli.txt",
+ "bzip2": "bzip2.txt",
+ "fmt": "fmt.txt",
+ "freetype": "freetype.txt",
+ "libogg": "ogg.txt",
+ "libpng": "libpng.txt",
+ "libtheora": "theora.txt",
+ "libvorbis": "vorbis.txt",
+ "openal-soft": "openal-soft.txt",
+ "zlib": "zlib.txt"
+ }
+ }
+ },
+ "files": [
+ {
+ "path": "angle/bin/d3dcompiler_47.dll",
+ "sha256": "A05F99734F7C4822FEFC12B367AF21FD0976ED6608752FB1E1E80B6ECE7ECBBB"
+ },
+ {
+ "path": "angle/bin/libEGL.dll",
+ "sha256": "B0E0C6BAC9D9F0F084DC8F56083A2EEFFB866C0629A64D52B5E33616B4F90CBE"
+ },
+ {
+ "path": "angle/bin/libGLESv2.dll",
+ "sha256": "6DE6940EDB728396CBA6F3C0D79172A8EFD33E37671A13B1721C652EECEBF4C4"
+ },
+ {
+ "path": "love/bin/love.dll",
+ "sha256": "A3622828014F03DDB502D2A7807DC8F94432172595BE03508099BD66937FD45F"
+ },
+ {
+ "path": "love/bin/lua51.dll",
+ "sha256": "08380FB9A7C8A8A85D3425C1FD6F1CB826D980FF31925453E353227E9A23BC70"
+ },
+ {
+ "path": "love/lib/liblove.lib",
+ "sha256": "C93A358D5EAE8BE3D74DDFDFA767657E96E95E0151F843FEB4E587CF2D89320A"
+ },
+ {
+ "path": "love/lib/lovestatic.lib",
+ "sha256": "15FB3A9CB3C3CDAB8B33DE5B1D1F8D8FF6CE9719E9AA7BC2CF3D7B51CDCE0C98"
+ },
+ {
+ "path": "love/lib/lua51.lib",
+ "sha256": "A4F4F0942A432B88A3541A185B3D3D5B4D58413B3CEEE10A2E48CAF9D8439759"
+ },
+ {
+ "path": "runtime/bin/brotlicommon.dll",
+ "sha256": "6A1D267564FE5BE6D601C1F5301F45DD0D0CCBDA2AE8975CACA4424A9FD452AF"
+ },
+ {
+ "path": "runtime/bin/brotlidec.dll",
+ "sha256": "C12C983AEE4E7EF966982AD11D71B32456CB445B18B8E11D9A35E781619085E2"
+ },
+ {
+ "path": "runtime/bin/bz2.dll",
+ "sha256": "28ADC0239EAF51D13F8C7075B8514420CAE92EFF14471EC6F5B56A11D9345F00"
+ },
+ {
+ "path": "runtime/bin/fmt.dll",
+ "sha256": "B4ED319D0733EA619D11770558957AFBDF99021D86A4ED068628AE34426C6090"
+ },
+ {
+ "path": "runtime/bin/freetype.dll",
+ "sha256": "3727B17849902FE12DED3C76BD71D2907C523CF67D1A7CB516025522E260F9D4"
+ },
+ {
+ "path": "runtime/bin/libpng16.dll",
+ "sha256": "9B3B823A59ABCB4BA43D1E6DB4C35855AE66384106E32FD5F9D486A7DECB81AF"
+ },
+ {
+ "path": "runtime/bin/ogg.dll",
+ "sha256": "283B0890599450AE4BE486C07241ECCC7B14B480465AF7F8D9F2A84528064A65"
+ },
+ {
+ "path": "runtime/bin/OpenAL32.dll",
+ "sha256": "D37D04CDD1EEE2005ACF64B558DFB16F0A0D83991F3B2A75E5C69FF314054F8B"
+ },
+ {
+ "path": "runtime/bin/theora.dll",
+ "sha256": "C90101C9C169B68F4FA155E63C71F28B271BBB68649085C8ACEFB26816663857"
+ },
+ {
+ "path": "runtime/bin/theoradec.dll",
+ "sha256": "53A5A3A0B24F72B7D8812DE4B78FD3BB222399B9293B577A8FCF80009B72F004"
+ },
+ {
+ "path": "runtime/bin/vorbis.dll",
+ "sha256": "A4B2D0A4C032A7E60AA297ADAE134E8A2C1BE61D7EC639CAA93AF85BF7EAC2C7"
+ },
+ {
+ "path": "runtime/bin/vorbisfile.dll",
+ "sha256": "0BFFA35539F0A8233A8EFF4C6680F8A7CF3B9DBE3B437A3F94D5301B66A2F134"
+ },
+ {
+ "path": "runtime/bin/z.dll",
+ "sha256": "8B86502ADC15A13C0E136FB9D63AA6583C59AD93EC608A9A7C901757354744C3"
+ },
+ {
+ "path": "sdl2/bin/SDL2.dll",
+ "sha256": "B02D13F77221A335AB9F21304F2B4E003FE59C6DF10F21BA6D49023016D35CA8"
+ },
+ {
+ "path": "sdl2/include/SDL2/begin_code.h",
+ "sha256": "9803DAB29FB4522CE3500AB789260E9622DF6307CE90F92AD20C1808E19C4E7B"
+ },
+ {
+ "path": "sdl2/include/SDL2/close_code.h",
+ "sha256": "4599E9CEDBA7451FF07A6306F0B7531BEED05B1C5A65B1A2613D1B771BE43FDA"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_assert.h",
+ "sha256": "32FC342E0A5F3A55EA1CE2A5D9195E77215EDCB6F4595C0DC3674DCCA1CC81D8"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_atomic.h",
+ "sha256": "C561CE8F94DE18CCB4F708BDA45880BA6D3C9F052E166BEB5DADC5544F35221E"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_audio.h",
+ "sha256": "047AFC29591AC43A90F06169F67EE7D99A6256C6BBE5EFE5DE79A52565BB6853"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_bits.h",
+ "sha256": "63D22D1C8FE0D83BBA5A49ABA9BB700592F14462F7F7BF87E5838064047E3FA9"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_blendmode.h",
+ "sha256": "1985F81B886D9BC681821AF4F23D28E6E51BF51F21E5EF28CCC13BC8C7CDFBAE"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_clipboard.h",
+ "sha256": "3E8C73FB9B3CA5A544B70B66B683B78DB53E1A187E066912A068C0ED7BB3E8CE"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_config.h",
+ "sha256": "108A953419E1638AE2FF09148E2927C3AC4FF3E15B72A56FF41B542F356021BA"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_copying.h",
+ "sha256": "3F90BE470C18F99F4AEF39444E9374EDBA166CFEB1B90F99F35286CC33924642"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_cpuinfo.h",
+ "sha256": "FEE0C489ED4364C21FE0B726A47F72DF167FBCBF42CF569807B2BBF77DC30C54"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_egl.h",
+ "sha256": "72C119F4F7EC30F13B03D3E33D2266CB012EE103F0AD1973786D84D538A0B53D"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_endian.h",
+ "sha256": "7907CB3AB7B8D7BC99274F5AEF5294BEF215B01EA49D97226CBE6E911DE557D5"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_error.h",
+ "sha256": "0931656A5825E5F1F319F2CD3F573FC527393BB02FA7A1ED3F7FAE6C790328BE"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_events.h",
+ "sha256": "CEB7BC717342E652E2C432FD1DD08A5508575CEA6BD8615FB4DDEA63C071DDC4"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_filesystem.h",
+ "sha256": "DCFAA010C73150E7B0D56F50F5ADB3FC2E35621091A55BC5436B2FB85FE71675"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_gamecontroller.h",
+ "sha256": "C77C1D8287EE441843B9D8BE87B71252BDB1CE76B7BFC4FCFF46889AD8E9CFEC"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_gesture.h",
+ "sha256": "A5A8F8BB8EFB26860FAA9694189AAF76DF1CDB7755CCD30A8599B46B237A5D82"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_guid.h",
+ "sha256": "DA8FEF1D047B31E55D1B167DE301A806481B6948E7C2497779FD71934C45D5DB"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_haptic.h",
+ "sha256": "4267B6F039BEA7AD0F5CB37C37D63D0704AF84F73A36D2680684FF302F49F153"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_hidapi.h",
+ "sha256": "489A83D1073E528F82E821C23C515AF8B5A59357EC5AB132C854BB588648F59F"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_hints.h",
+ "sha256": "96C1056E0734F019B1DAFED5129D4A9D36253559ED8C7577A23C188BB75A48C1"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_joystick.h",
+ "sha256": "A39FC5DA97EDC94EABFF8616A413CF850EFB35D1DAF437A8CC16CAA837766482"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_keyboard.h",
+ "sha256": "F68DFA4F784F0B22949A8DF94F3C3653EA01A03AE02297D17579EB9E7EA94054"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_keycode.h",
+ "sha256": "291C769A05BC3E1EAD011DB3E07CB52415E1AA306F484904D203E94CBB3186CD"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_loadso.h",
+ "sha256": "8D3653B7B774B406A289519C74AE695EB5B52D71060712A88FCE4C72D82B1BD3"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_locale.h",
+ "sha256": "C4A663CE8EA4CA22F961796CAE644A04FC521FA2D0681333F7DA8A581909560C"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_log.h",
+ "sha256": "3342CB31280C2DBF0E5809B03E5499F0CF5EDF193EFA5E3C5B17814526A92033"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_main.h",
+ "sha256": "622FFCD0E7FE0C755A987C5EB9DAD6B3672774026B8250B66AD3C40D3A4DDA9A"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_messagebox.h",
+ "sha256": "65DF549CAB0A515145AE55325C447159AE1D60171331A7508EAC06034D25C972"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_metal.h",
+ "sha256": "1F3D0251522B735B0E37677EB1A0594440599FBBB2AF86BBB508606CCA001A06"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_misc.h",
+ "sha256": "5D5E4B42481A5EF44EC9D66159E88426BE5632262731A3C3B223277771D30006"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_mouse.h",
+ "sha256": "66B5353387D011A54B232931E86121CB5551E251B9491B9ED15AFE293D5856FF"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_mutex.h",
+ "sha256": "C5EF9A8AEB056422EF878672770E1F60AE965933693DF6D8EBD53D0E514A1794"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_name.h",
+ "sha256": "FDC6C648734220285056AD8B8273511C75D5BE00D17FC1EA3EB4C06526B13FA3"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengl_glext.h",
+ "sha256": "1ABB28891C9B0661B6FF3749DE2671DA5FE12BEB5BCB1833B2D6BDF5C89ECDF8"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengl.h",
+ "sha256": "C4D2857E757B2B2A83D7531CBEDA19C383AD6D16648AB9B7544FC0A74EB2FF13"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengles.h",
+ "sha256": "0542791816FCDD84B74D98347AC1B00A97BC6C2CDF31334CD9083789FFB6594A"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengles2_gl2.h",
+ "sha256": "D6EC44B1D73F3AFCE3A20AC6976DB7FB3DAC656C5190F0B563E4CE90D79A15AA"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengles2_gl2ext.h",
+ "sha256": "4FC5B0034DCDE9C125922E3E70D01489A6B335076856EE660294E1A6305A3719"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengles2_gl2platform.h",
+ "sha256": "4779BE999ACD1904458238F09C86983B7960AC40E612E05DB2DB97DB615F1E0F"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengles2_khrplatform.h",
+ "sha256": "7B1E01AAA7AD8F6FC34B5C7BDF79EBF5189BB09E2C4D2E79FC5D350623D11E83"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_opengles2.h",
+ "sha256": "73A3B042F7B3D296904BFF91268730D5180638F4EC1836D57258FE53EE60DF8B"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_pixels.h",
+ "sha256": "FEE61AEE337A3823E832F5C26B3000315D6EB95DA91BA20D4FB0B5BAC9D4967A"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_platform.h",
+ "sha256": "9CE62823791D7A16B9AD17AD11F4DE8DCBCDE7918F67BF6D1DD4FC625B69E50C"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_power.h",
+ "sha256": "FBBDF3FF13B21095623EDFC5659C1B76C6DBB1364A4DD14D8F9E5CF329048784"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_quit.h",
+ "sha256": "9C902ABAEA7560F98FC546BD35A04629A205D047DF5433FC30B0C1D81AF58B21"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_rect.h",
+ "sha256": "01DB368D4B7736E2358CD29EEC0D187D964CFD6182CFFAB57A938ADF73B6A418"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_render.h",
+ "sha256": "A0CF0DB1A13D0DD0D8BFDC01FFB371E2B19BE36326F85058200AC9481876C7F0"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_revision.h",
+ "sha256": "EF28EADA36B000C710B033317A37677B5484A4DE224547CB7293A77FA0C97195"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_rwops.h",
+ "sha256": "10DA91C08E96E5FCA452E06E11029908A6F237356E4DB11AF1AD13FFA2E57A47"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_scancode.h",
+ "sha256": "EB14F3CEDDE58358F6456D185F473D7551E8234DE260151AD0319861FF1444C2"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_sensor.h",
+ "sha256": "F4DABF5BAE217FA6370D8AF73ABDCBBA99AEA626F35E17A381B636AFE01BB81B"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_shape.h",
+ "sha256": "CBDCDD8EAD6D61A7BFA09BC4C538CC598919D1C060D70524C0828D3AF7418667"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_stdinc.h",
+ "sha256": "33EDC0B35512B86162D1AFFE9DF95165B4BC514A90B17238420EFE85D24CA8A1"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_surface.h",
+ "sha256": "92299233370D645F78D9C655CE7F102C16F9D4DDDE28A59BD82A962C49B51D94"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_system.h",
+ "sha256": "FFC5F3662A85F795C0B2E2B87B9FBF493BCEEDA4AACF7721A68225C5E318CA67"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_syswm.h",
+ "sha256": "34FEC1A5F1089BD395EE11C3A25AD0FC10E7ABC56D4A77CC6F17EB9E4E8A9607"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_assert.h",
+ "sha256": "FE826FEB054929284764718C67AE95B920729AE1336FFEEC88A23D611E6498F0"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_common.h",
+ "sha256": "B619CC3020FFDC8F8A12F5D9D4AF8B83E9B7C6C3BFC05C6502933C4B3F0103DE"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_compare.h",
+ "sha256": "713AF284D8520E910D94090396BFD2DB2B2292096EDD93E4D60AA69163B9C510"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_crc32.h",
+ "sha256": "74CB4C2F7F7130D6048A2C716260D6403620F0E358FA928136ED3560BF71A378"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_font.h",
+ "sha256": "B0B7E6AC8BCF7B9E5BF28695B05D3A084CD530C90322CBC9A0B198114B174231"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_fuzzer.h",
+ "sha256": "494E0E49882A9C2745D07DA7B04761D8D992DD90A7A29DD34B605051ED8DCEE5"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_harness.h",
+ "sha256": "30B9CCB36E07FF0442429A27540F6A796AEE073355B538BD50EF0B98CC47982E"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_images.h",
+ "sha256": "35E7AD856A201FBAAFE7EC3FC48DB47FB5D9452C292267F8936AEC5E5C77B81D"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_log.h",
+ "sha256": "3A8736E6D757666420616F83E7929493FCBE816A0CDDF10347F3446818870BE8"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_md5.h",
+ "sha256": "BE73865B584B5EB207CAB3D8DCFF6AD057286B75053B547B0238DF275E282491"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_memory.h",
+ "sha256": "556F676637FBB1A85F1998545C59ADDCA358B8B95C71812337B370184F27483F"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test_random.h",
+ "sha256": "1D8C7987AE39B74D83F40FB54394FD32C61037716C77EBCAF7CE87788934C035"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_test.h",
+ "sha256": "55E453735FB6A14CF350F663E8C7BC9AA273DB95BB31BCF3ABBFB5B3A91A414E"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_thread.h",
+ "sha256": "0E546047DD407CA0E1D2D686CF2650AC77A987047184E3645F6BA2EC958DDC5C"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_timer.h",
+ "sha256": "D0497AD3D75701613A227134D63FDCBC62ED72F645153D52753E386EE4B498C7"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_touch.h",
+ "sha256": "4E5D7A083BBF5237DAF5CCF06FAED3FAE81B1CAA693E27841EBF4EC9BF8FF769"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_types.h",
+ "sha256": "ED0C92BED5EEC2648F744E8451F3AC5140C1EF11510175FA1B726273DBDED9ED"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_version.h",
+ "sha256": "EE97570D0D6507B4687076336C07FD4A7FA1F5E7986C9B61893A279950FEBCE8"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_video.h",
+ "sha256": "82AB63FF37834CD4BFCEE490B8E37FB892BBB0B31A1A7913EC13C329271CA5FF"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL_vulkan.h",
+ "sha256": "98ED6B3D354191019AE208FC861799237BA8FFB11FA6BD41B3569E6CE05555B8"
+ },
+ {
+ "path": "sdl2/include/SDL2/SDL.h",
+ "sha256": "952E89E1DC4E0DD1A7BEA6A153DF632B8CDB6E433A9B3B0702C5FFE15B9B9360"
+ },
+ {
+ "path": "sdl2/lib/SDL2.lib",
+ "sha256": "37B330EE42407B405BD2432E79CB9B9036245309B3ACC445AD70CB8E555B7288"
+ }
+ ]
+}
diff --git a/ports/uwp/third_party/runtime/README.md b/ports/uwp/third_party/runtime/README.md
new file mode 100644
index 00000000..9ec4890e
--- /dev/null
+++ b/ports/uwp/third_party/runtime/README.md
@@ -0,0 +1,5 @@
+# LÖVE runtime dependencies
+
+These x64 UWP Release DLLs are the runtime closure used by the bundled LÖVE backend. They were installed by vcpkg at commit `80f9bcfa455e875d9c1bf7a7c6692d7e1e481061`.
+
+The bundle contains Brotli 1.2.0, bzip2 1.0.8#6, fmt 12.1.0, FreeType 2.14.3, libogg 1.3.6#1, libpng 1.6.58, libtheora 1.2.0, libvorbis 1.3.7#4, OpenAL Soft 1.25.1, and zlib 1.3.2. Corresponding notices are under `../licenses`.
diff --git a/ports/uwp/third_party/runtime/bin/OpenAL32.dll b/ports/uwp/third_party/runtime/bin/OpenAL32.dll
new file mode 100644
index 00000000..ea0e14d2
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/OpenAL32.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/brotlicommon.dll b/ports/uwp/third_party/runtime/bin/brotlicommon.dll
new file mode 100644
index 00000000..459dace0
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/brotlicommon.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/brotlidec.dll b/ports/uwp/third_party/runtime/bin/brotlidec.dll
new file mode 100644
index 00000000..7ece67e5
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/brotlidec.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/bz2.dll b/ports/uwp/third_party/runtime/bin/bz2.dll
new file mode 100644
index 00000000..a5eff563
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/bz2.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/fmt.dll b/ports/uwp/third_party/runtime/bin/fmt.dll
new file mode 100644
index 00000000..ae9a9d2a
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/fmt.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/freetype.dll b/ports/uwp/third_party/runtime/bin/freetype.dll
new file mode 100644
index 00000000..1e431bb1
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/freetype.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/libpng16.dll b/ports/uwp/third_party/runtime/bin/libpng16.dll
new file mode 100644
index 00000000..fddc7647
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/libpng16.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/ogg.dll b/ports/uwp/third_party/runtime/bin/ogg.dll
new file mode 100644
index 00000000..3659783e
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/ogg.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/theora.dll b/ports/uwp/third_party/runtime/bin/theora.dll
new file mode 100644
index 00000000..e7861703
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/theora.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/theoradec.dll b/ports/uwp/third_party/runtime/bin/theoradec.dll
new file mode 100644
index 00000000..c9926bfb
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/theoradec.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/vorbis.dll b/ports/uwp/third_party/runtime/bin/vorbis.dll
new file mode 100644
index 00000000..3a72086e
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/vorbis.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/vorbisfile.dll b/ports/uwp/third_party/runtime/bin/vorbisfile.dll
new file mode 100644
index 00000000..ee9cc728
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/vorbisfile.dll differ
diff --git a/ports/uwp/third_party/runtime/bin/z.dll b/ports/uwp/third_party/runtime/bin/z.dll
new file mode 100644
index 00000000..66dc2628
Binary files /dev/null and b/ports/uwp/third_party/runtime/bin/z.dll differ
diff --git a/ports/uwp/third_party/sdl2/README.md b/ports/uwp/third_party/sdl2/README.md
new file mode 100644
index 00000000..3883d0d8
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/README.md
@@ -0,0 +1,9 @@
+# SDL2 UWP binaries
+
+The bundled x64 UWP Release binaries use SDL 2.32.10 from the official `release-2.32.10` commit recorded in the dependency manifest. The rebuild script creates `source` and applies the WinRT only controller changes there.
+
+`patches/xbox-wgi-controller.patch` records those changes for review. They read Xbox gamepads through `IGamepad::GetCurrentReading` and expose the standard SDL button, axis, trigger, and D-pad layout.
+
+`SDL2.dll`, `SDL2.lib`, and the headers must be updated together.
+
+`scripts/xbox-uwp/build_sdl2_angle.ps1` builds the source for x64 WindowsStore with GLES enabled and desktop OpenGL and Vulkan disabled. ANGLE supplies EGL and GLES at runtime.
diff --git a/ports/uwp/third_party/sdl2/bin/SDL2.dll b/ports/uwp/third_party/sdl2/bin/SDL2.dll
new file mode 100644
index 00000000..0ee42904
Binary files /dev/null and b/ports/uwp/third_party/sdl2/bin/SDL2.dll differ
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL.h
new file mode 100644
index 00000000..0fe07133
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL.h
@@ -0,0 +1,234 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * \file SDL.h
+ *
+ * Main include header for the SDL library
+ */
+
+#ifndef SDL_h_
+#define SDL_h_
+
+#include "SDL_main.h"
+#include "SDL_stdinc.h"
+#include "SDL_assert.h"
+#include "SDL_atomic.h"
+#include "SDL_audio.h"
+#include "SDL_clipboard.h"
+#include "SDL_cpuinfo.h"
+#include "SDL_endian.h"
+#include "SDL_error.h"
+#include "SDL_events.h"
+#include "SDL_filesystem.h"
+#include "SDL_gamecontroller.h"
+#include "SDL_guid.h"
+#include "SDL_haptic.h"
+#include "SDL_hidapi.h"
+#include "SDL_hints.h"
+#include "SDL_joystick.h"
+#include "SDL_loadso.h"
+#include "SDL_log.h"
+#include "SDL_messagebox.h"
+#include "SDL_metal.h"
+#include "SDL_mutex.h"
+#include "SDL_power.h"
+#include "SDL_render.h"
+#include "SDL_rwops.h"
+#include "SDL_sensor.h"
+#include "SDL_shape.h"
+#include "SDL_system.h"
+#include "SDL_thread.h"
+#include "SDL_timer.h"
+#include "SDL_version.h"
+#include "SDL_video.h"
+#include "SDL_locale.h"
+#include "SDL_misc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* WIKI CATEGORY: Init */
+
+/* As of version 0.5, SDL is loaded dynamically into the application */
+
+/**
+ * \name SDL_INIT_*
+ *
+ * These are the flags which may be passed to SDL_Init(). You should
+ * specify the subsystems which you will be using in your application.
+ */
+/* @{ */
+#define SDL_INIT_TIMER 0x00000001u
+#define SDL_INIT_AUDIO 0x00000010u
+#define SDL_INIT_VIDEO 0x00000020u /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */
+#define SDL_INIT_JOYSTICK 0x00000200u /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */
+#define SDL_INIT_HAPTIC 0x00001000u
+#define SDL_INIT_GAMECONTROLLER 0x00002000u /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
+#define SDL_INIT_EVENTS 0x00004000u
+#define SDL_INIT_SENSOR 0x00008000u
+#define SDL_INIT_NOPARACHUTE 0x00100000u /**< compatibility; this flag is ignored. */
+#define SDL_INIT_EVERYTHING ( \
+ SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
+ SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \
+ )
+/* @} */
+
+/**
+ * Initialize the SDL library.
+ *
+ * SDL_Init() simply forwards to calling SDL_InitSubSystem(). Therefore, the
+ * two may be used interchangeably. Though for readability of your code
+ * SDL_InitSubSystem() might be preferred.
+ *
+ * The file I/O (for example: SDL_RWFromFile) and threading (SDL_CreateThread)
+ * subsystems are initialized by default. Message boxes
+ * (SDL_ShowSimpleMessageBox) also attempt to work without initializing the
+ * video subsystem, in hopes of being useful in showing an error dialog when
+ * SDL_Init fails. You must specifically initialize other subsystems if you
+ * use them in your application.
+ *
+ * Logging (such as SDL_Log) works without initialization, too.
+ *
+ * `flags` may be any of the following OR'd together:
+ *
+ * - `SDL_INIT_TIMER`: timer subsystem
+ * - `SDL_INIT_AUDIO`: audio subsystem
+ * - `SDL_INIT_VIDEO`: video subsystem; automatically initializes the events
+ * subsystem
+ * - `SDL_INIT_JOYSTICK`: joystick subsystem; automatically initializes the
+ * events subsystem
+ * - `SDL_INIT_HAPTIC`: haptic (force feedback) subsystem
+ * - `SDL_INIT_GAMECONTROLLER`: controller subsystem; automatically
+ * initializes the joystick subsystem
+ * - `SDL_INIT_EVENTS`: events subsystem
+ * - `SDL_INIT_EVERYTHING`: all of the above subsystems
+ * - `SDL_INIT_NOPARACHUTE`: compatibility; this flag is ignored
+ *
+ * Subsystem initialization is ref-counted, you must call SDL_QuitSubSystem()
+ * for each SDL_InitSubSystem() to correctly shutdown a subsystem manually (or
+ * call SDL_Quit() to force shutdown). If a subsystem is already loaded then
+ * this call will increase the ref-count and return.
+ *
+ * \param flags subsystem initialization flags.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_InitSubSystem
+ * \sa SDL_Quit
+ * \sa SDL_SetMainReady
+ * \sa SDL_WasInit
+ */
+extern DECLSPEC int SDLCALL SDL_Init(Uint32 flags);
+
+/**
+ * Compatibility function to initialize the SDL library.
+ *
+ * In SDL2, this function and SDL_Init() are interchangeable.
+ *
+ * \param flags any of the flags used by SDL_Init(); see SDL_Init for details.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Init
+ * \sa SDL_Quit
+ * \sa SDL_QuitSubSystem
+ */
+extern DECLSPEC int SDLCALL SDL_InitSubSystem(Uint32 flags);
+
+/**
+ * Shut down specific SDL subsystems.
+ *
+ * If you start a subsystem using a call to that subsystem's init function
+ * (for example SDL_VideoInit()) instead of SDL_Init() or SDL_InitSubSystem(),
+ * SDL_QuitSubSystem() and SDL_WasInit() will not work. You will need to use
+ * that subsystem's quit function (SDL_VideoQuit()) directly instead. But
+ * generally, you should not be using those functions directly anyhow; use
+ * SDL_Init() instead.
+ *
+ * You still need to call SDL_Quit() even if you close all open subsystems
+ * with SDL_QuitSubSystem().
+ *
+ * \param flags any of the flags used by SDL_Init(); see SDL_Init for details.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_InitSubSystem
+ * \sa SDL_Quit
+ */
+extern DECLSPEC void SDLCALL SDL_QuitSubSystem(Uint32 flags);
+
+/**
+ * Get a mask of the specified subsystems which are currently initialized.
+ *
+ * \param flags any of the flags used by SDL_Init(); see SDL_Init for details.
+ * \returns a mask of all initialized subsystems if `flags` is 0, otherwise it
+ * returns the initialization status of the specified subsystems.
+ *
+ * The return value does not include SDL_INIT_NOPARACHUTE.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Init
+ * \sa SDL_InitSubSystem
+ */
+extern DECLSPEC Uint32 SDLCALL SDL_WasInit(Uint32 flags);
+
+/**
+ * Clean up all initialized subsystems.
+ *
+ * You should call this function even if you have already shutdown each
+ * initialized subsystem with SDL_QuitSubSystem(). It is safe to call this
+ * function even in the case of errors in initialization.
+ *
+ * If you start a subsystem using a call to that subsystem's init function
+ * (for example SDL_VideoInit()) instead of SDL_Init() or SDL_InitSubSystem(),
+ * then you must use that subsystem's quit function (SDL_VideoQuit()) to shut
+ * it down before calling SDL_Quit(). But generally, you should not be using
+ * those functions directly anyhow; use SDL_Init() instead.
+ *
+ * You can use this function with atexit() to ensure that it is run when your
+ * application is shutdown, but it is not wise to do this from a library or
+ * other dynamically loaded code.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Init
+ * \sa SDL_QuitSubSystem
+ */
+extern DECLSPEC void SDLCALL SDL_Quit(void);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_assert.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_assert.h
new file mode 100644
index 00000000..5d7e19ae
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_assert.h
@@ -0,0 +1,324 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_assert_h_
+#define SDL_assert_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef SDL_ASSERT_LEVEL
+#ifdef SDL_DEFAULT_ASSERT_LEVEL
+#define SDL_ASSERT_LEVEL SDL_DEFAULT_ASSERT_LEVEL
+#elif defined(_DEBUG) || defined(DEBUG) || \
+ (defined(__GNUC__) && !defined(__OPTIMIZE__))
+#define SDL_ASSERT_LEVEL 2
+#else
+#define SDL_ASSERT_LEVEL 1
+#endif
+#endif /* SDL_ASSERT_LEVEL */
+
+/*
+These are macros and not first class functions so that the debugger breaks
+on the assertion line and not in some random guts of SDL, and so each
+assert can have unique static variables associated with it.
+*/
+
+#if defined(_MSC_VER)
+/* Don't include intrin.h here because it contains C++ code */
+ extern void __cdecl __debugbreak(void);
+ #define SDL_TriggerBreakpoint() __debugbreak()
+#elif _SDL_HAS_BUILTIN(__builtin_debugtrap)
+ #define SDL_TriggerBreakpoint() __builtin_debugtrap()
+#elif ( (!defined(__NACL__)) && ((defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__))) )
+ #define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "int $3\n\t" )
+#elif (defined(__GNUC__) || defined(__clang__)) && defined(__riscv)
+ #define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "ebreak\n\t" )
+#elif ( defined(__APPLE__) && (defined(__arm64__) || defined(__aarch64__)) ) /* this might work on other ARM targets, but this is a known quantity... */
+ #define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "brk #22\n\t" )
+#elif defined(__APPLE__) && defined(__arm__)
+ #define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "bkpt #22\n\t" )
+#elif defined(_WIN32) && ((defined(__GNUC__) || defined(__clang__)) && (defined(__arm64__) || defined(__aarch64__)) )
+ #define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "brk #0xF000\n\t" )
+#elif defined(__386__) && defined(__WATCOMC__)
+ #define SDL_TriggerBreakpoint() { _asm { int 0x03 } }
+#elif defined(HAVE_SIGNAL_H) && !defined(__WATCOMC__)
+ #include
+ #define SDL_TriggerBreakpoint() raise(SIGTRAP)
+#else
+ /* How do we trigger breakpoints on this platform? */
+ #define SDL_TriggerBreakpoint()
+#endif
+
+#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 supports __func__ as a standard. */
+# define SDL_FUNCTION __func__
+#elif ((defined(__GNUC__) && (__GNUC__ >= 2)) || defined(_MSC_VER) || defined (__WATCOMC__))
+# define SDL_FUNCTION __FUNCTION__
+#else
+# define SDL_FUNCTION "???"
+#endif
+#define SDL_FILE __FILE__
+#define SDL_LINE __LINE__
+
+/*
+sizeof (x) makes the compiler still parse the expression even without
+assertions enabled, so the code is always checked at compile time, but
+doesn't actually generate code for it, so there are no side effects or
+expensive checks at run time, just the constant size of what x WOULD be,
+which presumably gets optimized out as unused.
+This also solves the problem of...
+
+ int somevalue = blah();
+ SDL_assert(somevalue == 1);
+
+...which would cause compiles to complain that somevalue is unused if we
+disable assertions.
+*/
+
+/* "while (0,0)" fools Microsoft's compiler's /W4 warning level into thinking
+ this condition isn't constant. And looks like an owl's face! */
+#ifdef _MSC_VER /* stupid /W4 warnings. */
+#define SDL_NULL_WHILE_LOOP_CONDITION (0,0)
+#else
+#define SDL_NULL_WHILE_LOOP_CONDITION (0)
+#endif
+
+#define SDL_disabled_assert(condition) \
+ do { (void) sizeof ((condition)); } while (SDL_NULL_WHILE_LOOP_CONDITION)
+
+typedef enum
+{
+ SDL_ASSERTION_RETRY, /**< Retry the assert immediately. */
+ SDL_ASSERTION_BREAK, /**< Make the debugger trigger a breakpoint. */
+ SDL_ASSERTION_ABORT, /**< Terminate the program. */
+ SDL_ASSERTION_IGNORE, /**< Ignore the assert. */
+ SDL_ASSERTION_ALWAYS_IGNORE /**< Ignore the assert from now on. */
+} SDL_AssertState;
+
+typedef struct SDL_AssertData
+{
+ int always_ignore;
+ unsigned int trigger_count;
+ const char *condition;
+ const char *filename;
+ int linenum;
+ const char *function;
+ const struct SDL_AssertData *next;
+} SDL_AssertData;
+
+/* Never call this directly. Use the SDL_assert* macros. */
+extern DECLSPEC SDL_AssertState SDLCALL SDL_ReportAssertion(SDL_AssertData *,
+ const char *,
+ const char *, int)
+#if defined(__clang__)
+#if __has_feature(attribute_analyzer_noreturn)
+/* this tells Clang's static analysis that we're a custom assert function,
+ and that the analyzer should assume the condition was always true past this
+ SDL_assert test. */
+ __attribute__((analyzer_noreturn))
+#endif
+#endif
+;
+
+/* the do {} while(0) avoids dangling else problems:
+ if (x) SDL_assert(y); else blah();
+ ... without the do/while, the "else" could attach to this macro's "if".
+ We try to handle just the minimum we need here in a macro...the loop,
+ the static vars, and break points. The heavy lifting is handled in
+ SDL_ReportAssertion(), in SDL_assert.c.
+*/
+#define SDL_enabled_assert(condition) \
+ do { \
+ while ( !(condition) ) { \
+ static struct SDL_AssertData sdl_assert_data = { 0, 0, #condition, NULL, 0, NULL, NULL }; \
+ const SDL_AssertState sdl_assert_state = SDL_ReportAssertion(&sdl_assert_data, SDL_FUNCTION, SDL_FILE, SDL_LINE); \
+ if (sdl_assert_state == SDL_ASSERTION_RETRY) { \
+ continue; /* go again. */ \
+ } else if (sdl_assert_state == SDL_ASSERTION_BREAK) { \
+ SDL_TriggerBreakpoint(); \
+ } \
+ break; /* not retrying. */ \
+ } \
+ } while (SDL_NULL_WHILE_LOOP_CONDITION)
+
+/* Enable various levels of assertions. */
+#if SDL_ASSERT_LEVEL == 0 /* assertions disabled */
+# define SDL_assert(condition) SDL_disabled_assert(condition)
+# define SDL_assert_release(condition) SDL_disabled_assert(condition)
+# define SDL_assert_paranoid(condition) SDL_disabled_assert(condition)
+#elif SDL_ASSERT_LEVEL == 1 /* release settings. */
+# define SDL_assert(condition) SDL_disabled_assert(condition)
+# define SDL_assert_release(condition) SDL_enabled_assert(condition)
+# define SDL_assert_paranoid(condition) SDL_disabled_assert(condition)
+#elif SDL_ASSERT_LEVEL == 2 /* normal settings. */
+# define SDL_assert(condition) SDL_enabled_assert(condition)
+# define SDL_assert_release(condition) SDL_enabled_assert(condition)
+# define SDL_assert_paranoid(condition) SDL_disabled_assert(condition)
+#elif SDL_ASSERT_LEVEL == 3 /* paranoid settings. */
+# define SDL_assert(condition) SDL_enabled_assert(condition)
+# define SDL_assert_release(condition) SDL_enabled_assert(condition)
+# define SDL_assert_paranoid(condition) SDL_enabled_assert(condition)
+#else
+# error Unknown assertion level.
+#endif
+
+/* this assertion is never disabled at any level. */
+#define SDL_assert_always(condition) SDL_enabled_assert(condition)
+
+
+/**
+ * A callback that fires when an SDL assertion fails.
+ *
+ * \param data a pointer to the SDL_AssertData structure corresponding to the
+ * current assertion.
+ * \param userdata what was passed as `userdata` to SDL_SetAssertionHandler().
+ * \returns an SDL_AssertState value indicating how to handle the failure.
+ */
+typedef SDL_AssertState (SDLCALL *SDL_AssertionHandler)(
+ const SDL_AssertData* data, void* userdata);
+
+/**
+ * Set an application-defined assertion handler.
+ *
+ * This function allows an application to show its own assertion UI and/or
+ * force the response to an assertion failure. If the application doesn't
+ * provide this, SDL will try to do the right thing, popping up a
+ * system-specific GUI dialog, and probably minimizing any fullscreen windows.
+ *
+ * This callback may fire from any thread, but it runs wrapped in a mutex, so
+ * it will only fire from one thread at a time.
+ *
+ * This callback is NOT reset to SDL's internal handler upon SDL_Quit()!
+ *
+ * \param handler the SDL_AssertionHandler function to call when an assertion
+ * fails or NULL for the default handler.
+ * \param userdata a pointer that is passed to `handler`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetAssertionHandler
+ */
+extern DECLSPEC void SDLCALL SDL_SetAssertionHandler(
+ SDL_AssertionHandler handler,
+ void *userdata);
+
+/**
+ * Get the default assertion handler.
+ *
+ * This returns the function pointer that is called by default when an
+ * assertion is triggered. This is an internal function provided by SDL, that
+ * is used for assertions when SDL_SetAssertionHandler() hasn't been used to
+ * provide a different function.
+ *
+ * \returns the default SDL_AssertionHandler that is called when an assert
+ * triggers.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_GetAssertionHandler
+ */
+extern DECLSPEC SDL_AssertionHandler SDLCALL SDL_GetDefaultAssertionHandler(void);
+
+/**
+ * Get the current assertion handler.
+ *
+ * This returns the function pointer that is called when an assertion is
+ * triggered. This is either the value last passed to
+ * SDL_SetAssertionHandler(), or if no application-specified function is set,
+ * is equivalent to calling SDL_GetDefaultAssertionHandler().
+ *
+ * The parameter `puserdata` is a pointer to a void*, which will store the
+ * "userdata" pointer that was passed to SDL_SetAssertionHandler(). This value
+ * will always be NULL for the default handler. If you don't care about this
+ * data, it is safe to pass a NULL pointer to this function to ignore it.
+ *
+ * \param puserdata pointer which is filled with the "userdata" pointer that
+ * was passed to SDL_SetAssertionHandler().
+ * \returns the SDL_AssertionHandler that is called when an assert triggers.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_SetAssertionHandler
+ */
+extern DECLSPEC SDL_AssertionHandler SDLCALL SDL_GetAssertionHandler(void **puserdata);
+
+/**
+ * Get a list of all assertion failures.
+ *
+ * This function gets all assertions triggered since the last call to
+ * SDL_ResetAssertionReport(), or the start of the program.
+ *
+ * The proper way to examine this data looks something like this:
+ *
+ * ```c
+ * const SDL_AssertData *item = SDL_GetAssertionReport();
+ * while (item) {
+ * printf("'%s', %s (%s:%d), triggered %u times, always ignore: %s.\\n",
+ * item->condition, item->function, item->filename,
+ * item->linenum, item->trigger_count,
+ * item->always_ignore ? "yes" : "no");
+ * item = item->next;
+ * }
+ * ```
+ *
+ * \returns a list of all failed assertions or NULL if the list is empty. This
+ * memory should not be modified or freed by the application.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_ResetAssertionReport
+ */
+extern DECLSPEC const SDL_AssertData * SDLCALL SDL_GetAssertionReport(void);
+
+/**
+ * Clear the list of all assertion failures.
+ *
+ * This function will clear the list of all assertions triggered up to that
+ * point. Immediately following this call, SDL_GetAssertionReport will return
+ * no items. In addition, any previously-triggered assertions will be reset to
+ * a trigger_count of zero, and their always_ignore state will be false.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetAssertionReport
+ */
+extern DECLSPEC void SDLCALL SDL_ResetAssertionReport(void);
+
+
+/* these had wrong naming conventions until 2.0.4. Please update your app! */
+#define SDL_assert_state SDL_AssertState
+#define SDL_assert_data SDL_AssertData
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_assert_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_atomic.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_atomic.h
new file mode 100644
index 00000000..226ec7c6
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_atomic.h
@@ -0,0 +1,408 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryAtomic
+ *
+ * Atomic operations.
+ *
+ * IMPORTANT: If you are not an expert in concurrent lockless programming, you
+ * should not be using any functions in this file. You should be protecting
+ * your data structures with full mutexes instead.
+ *
+ * ***Seriously, here be dragons!***
+ *
+ * You can find out a little more about lockless programming and the subtle
+ * issues that can arise here:
+ * https://learn.microsoft.com/en-us/windows/win32/dxtecharts/lockless-programming
+ *
+ * There's also lots of good information here:
+ *
+ * - https://www.1024cores.net/home/lock-free-algorithms
+ * - https://preshing.com/
+ *
+ * These operations may or may not actually be implemented using processor
+ * specific atomic operations. When possible they are implemented as true
+ * processor specific atomic operations. When that is not possible the are
+ * implemented using locks that *do* use the available atomic operations.
+ *
+ * All of the atomic operations that modify memory are full memory barriers.
+ */
+
+#ifndef SDL_atomic_h_
+#define SDL_atomic_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_platform.h"
+
+#include "begin_code.h"
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \name SDL AtomicLock
+ *
+ * The atomic locks are efficient spinlocks using CPU instructions,
+ * but are vulnerable to starvation and can spin forever if a thread
+ * holding a lock has been terminated. For this reason you should
+ * minimize the code executed inside an atomic lock and never do
+ * expensive things like API or system calls while holding them.
+ *
+ * The atomic locks are not safe to lock recursively.
+ *
+ * Porting Note:
+ * The spin lock functions and type are required and can not be
+ * emulated because they are used in the atomic emulation code.
+ */
+/* @{ */
+
+typedef int SDL_SpinLock;
+
+/**
+ * Try to lock a spin lock by setting it to a non-zero value.
+ *
+ * ***Please note that spinlocks are dangerous if you don't know what you're
+ * doing. Please be careful using any sort of spinlock!***
+ *
+ * \param lock a pointer to a lock variable.
+ * \returns SDL_TRUE if the lock succeeded, SDL_FALSE if the lock is already
+ * held.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AtomicLock
+ * \sa SDL_AtomicUnlock
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_AtomicTryLock(SDL_SpinLock *lock);
+
+/**
+ * Lock a spin lock by setting it to a non-zero value.
+ *
+ * ***Please note that spinlocks are dangerous if you don't know what you're
+ * doing. Please be careful using any sort of spinlock!***
+ *
+ * \param lock a pointer to a lock variable.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AtomicTryLock
+ * \sa SDL_AtomicUnlock
+ */
+extern DECLSPEC void SDLCALL SDL_AtomicLock(SDL_SpinLock *lock);
+
+/**
+ * Unlock a spin lock by setting it to 0.
+ *
+ * Always returns immediately.
+ *
+ * ***Please note that spinlocks are dangerous if you don't know what you're
+ * doing. Please be careful using any sort of spinlock!***
+ *
+ * \param lock a pointer to a lock variable.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AtomicLock
+ * \sa SDL_AtomicTryLock
+ */
+extern DECLSPEC void SDLCALL SDL_AtomicUnlock(SDL_SpinLock *lock);
+
+/* @} *//* SDL AtomicLock */
+
+
+/**
+ * The compiler barrier prevents the compiler from reordering
+ * reads and writes to globally visible variables across the call.
+ */
+#if defined(_MSC_VER) && (_MSC_VER > 1200) && !defined(__clang__)
+void _ReadWriteBarrier(void);
+#pragma intrinsic(_ReadWriteBarrier)
+#define SDL_CompilerBarrier() _ReadWriteBarrier()
+#elif (defined(__GNUC__) && !defined(__EMSCRIPTEN__)) || (defined(__SUNPRO_C) && (__SUNPRO_C >= 0x5120))
+/* This is correct for all CPUs when using GCC or Solaris Studio 12.1+. */
+#define SDL_CompilerBarrier() __asm__ __volatile__ ("" : : : "memory")
+#elif defined(__WATCOMC__)
+extern __inline void SDL_CompilerBarrier(void);
+#pragma aux SDL_CompilerBarrier = "" parm [] modify exact [];
+#else
+#define SDL_CompilerBarrier() \
+{ SDL_SpinLock _tmp = 0; SDL_AtomicLock(&_tmp); SDL_AtomicUnlock(&_tmp); }
+#endif
+
+/**
+ * Memory barriers are designed to prevent reads and writes from being
+ * reordered by the compiler and being seen out of order on multi-core CPUs.
+ *
+ * A typical pattern would be for thread A to write some data and a flag, and
+ * for thread B to read the flag and get the data. In this case you would
+ * insert a release barrier between writing the data and the flag,
+ * guaranteeing that the data write completes no later than the flag is
+ * written, and you would insert an acquire barrier between reading the flag
+ * and reading the data, to ensure that all the reads associated with the flag
+ * have completed.
+ *
+ * In this pattern you should always see a release barrier paired with an
+ * acquire barrier and you should gate the data reads/writes with a single
+ * flag variable.
+ *
+ * For more information on these semantics, take a look at the blog post:
+ * http://preshing.com/20120913/acquire-and-release-semantics
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC void SDLCALL SDL_MemoryBarrierReleaseFunction(void);
+extern DECLSPEC void SDLCALL SDL_MemoryBarrierAcquireFunction(void);
+
+#if defined(__GNUC__) && (defined(__powerpc__) || defined(__ppc__))
+#define SDL_MemoryBarrierRelease() __asm__ __volatile__ ("lwsync" : : : "memory")
+#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("lwsync" : : : "memory")
+#elif defined(__GNUC__) && defined(__aarch64__)
+#define SDL_MemoryBarrierRelease() __asm__ __volatile__ ("dmb ish" : : : "memory")
+#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("dmb ish" : : : "memory")
+#elif defined(__GNUC__) && defined(__arm__)
+#if 0 /* defined(__LINUX__) || defined(__ANDROID__) */
+/* Information from:
+ https://chromium.googlesource.com/chromium/chromium/+/trunk/base/atomicops_internals_arm_gcc.h#19
+
+ The Linux kernel provides a helper function which provides the right code for a memory barrier,
+ hard-coded at address 0xffff0fa0
+*/
+typedef void (*SDL_KernelMemoryBarrierFunc)();
+#define SDL_MemoryBarrierRelease() ((SDL_KernelMemoryBarrierFunc)0xffff0fa0)()
+#define SDL_MemoryBarrierAcquire() ((SDL_KernelMemoryBarrierFunc)0xffff0fa0)()
+#elif 0 /* defined(__QNXNTO__) */
+#include
+
+#define SDL_MemoryBarrierRelease() __cpu_membarrier()
+#define SDL_MemoryBarrierAcquire() __cpu_membarrier()
+#else
+#if defined(__ARM_ARCH_7__) || defined(__ARM_ARCH_7A__) || defined(__ARM_ARCH_7EM__) || defined(__ARM_ARCH_7R__) || defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7S__) || defined(__ARM_ARCH_8A__)
+#define SDL_MemoryBarrierRelease() __asm__ __volatile__ ("dmb ish" : : : "memory")
+#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("dmb ish" : : : "memory")
+#elif defined(__ARM_ARCH_6__) || defined(__ARM_ARCH_6J__) || defined(__ARM_ARCH_6K__) || defined(__ARM_ARCH_6T2__) || defined(__ARM_ARCH_6Z__) || defined(__ARM_ARCH_6ZK__)
+#ifdef __thumb__
+/* The mcr instruction isn't available in thumb mode, use real functions */
+#define SDL_MEMORY_BARRIER_USES_FUNCTION
+#define SDL_MemoryBarrierRelease() SDL_MemoryBarrierReleaseFunction()
+#define SDL_MemoryBarrierAcquire() SDL_MemoryBarrierAcquireFunction()
+#else
+#define SDL_MemoryBarrierRelease() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c10, 5" : : "r"(0) : "memory")
+#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c10, 5" : : "r"(0) : "memory")
+#endif /* __thumb__ */
+#else
+#define SDL_MemoryBarrierRelease() __asm__ __volatile__ ("" : : : "memory")
+#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("" : : : "memory")
+#endif /* __LINUX__ || __ANDROID__ */
+#endif /* __GNUC__ && __arm__ */
+#else
+#if (defined(__SUNPRO_C) && (__SUNPRO_C >= 0x5120))
+/* This is correct for all CPUs on Solaris when using Solaris Studio 12.1+. */
+#include
+#define SDL_MemoryBarrierRelease() __machine_rel_barrier()
+#define SDL_MemoryBarrierAcquire() __machine_acq_barrier()
+#else
+/* This is correct for the x86 and x64 CPUs, and we'll expand this over time. */
+#define SDL_MemoryBarrierRelease() SDL_CompilerBarrier()
+#define SDL_MemoryBarrierAcquire() SDL_CompilerBarrier()
+#endif
+#endif
+
+/* "REP NOP" is PAUSE, coded for tools that don't know it by that name. */
+#if (defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__))
+ #define SDL_CPUPauseInstruction() __asm__ __volatile__("pause\n") /* Some assemblers can't do REP NOP, so go with PAUSE. */
+#elif (defined(__arm__) && defined(__ARM_ARCH) && __ARM_ARCH >= 7) || defined(__aarch64__)
+ #define SDL_CPUPauseInstruction() __asm__ __volatile__("yield" ::: "memory")
+#elif (defined(__powerpc__) || defined(__powerpc64__))
+ #define SDL_CPUPauseInstruction() __asm__ __volatile__("or 27,27,27");
+#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
+ #define SDL_CPUPauseInstruction() _mm_pause() /* this is actually "rep nop" and not a SIMD instruction. No inline asm in MSVC x86-64! */
+#elif defined(_MSC_VER) && (defined(_M_ARM) || defined(_M_ARM64))
+ #define SDL_CPUPauseInstruction() __yield()
+#elif defined(__WATCOMC__) && defined(__386__)
+ extern __inline void SDL_CPUPauseInstruction(void);
+ #pragma aux SDL_CPUPauseInstruction = ".686p" ".xmm2" "pause"
+#else
+ #define SDL_CPUPauseInstruction()
+#endif
+
+
+/**
+ * A type representing an atomic integer value.
+ *
+ * It is a struct so people don't accidentally use numeric operations on it.
+ */
+typedef struct SDL_atomic_t {
+ int value;
+} SDL_atomic_t;
+
+/**
+ * Set an atomic variable to a new value if it is currently an old value.
+ *
+ * ***Note: If you don't know what this function is for, you shouldn't use
+ * it!***
+ *
+ * \param a a pointer to an SDL_atomic_t variable to be modified.
+ * \param oldval the old value.
+ * \param newval the new value.
+ * \returns SDL_TRUE if the atomic variable was set, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AtomicCASPtr
+ * \sa SDL_AtomicGet
+ * \sa SDL_AtomicSet
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_AtomicCAS(SDL_atomic_t *a, int oldval, int newval);
+
+/**
+ * Set an atomic variable to a value.
+ *
+ * This function also acts as a full memory barrier.
+ *
+ * ***Note: If you don't know what this function is for, you shouldn't use
+ * it!***
+ *
+ * \param a a pointer to an SDL_atomic_t variable to be modified.
+ * \param v the desired value.
+ * \returns the previous value of the atomic variable.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_AtomicGet
+ */
+extern DECLSPEC int SDLCALL SDL_AtomicSet(SDL_atomic_t *a, int v);
+
+/**
+ * Get the value of an atomic variable.
+ *
+ * ***Note: If you don't know what this function is for, you shouldn't use
+ * it!***
+ *
+ * \param a a pointer to an SDL_atomic_t variable.
+ * \returns the current value of an atomic variable.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_AtomicSet
+ */
+extern DECLSPEC int SDLCALL SDL_AtomicGet(SDL_atomic_t *a);
+
+/**
+ * Add to an atomic variable.
+ *
+ * This function also acts as a full memory barrier.
+ *
+ * ***Note: If you don't know what this function is for, you shouldn't use
+ * it!***
+ *
+ * \param a a pointer to an SDL_atomic_t variable to be modified.
+ * \param v the desired value to add.
+ * \returns the previous value of the atomic variable.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_AtomicDecRef
+ * \sa SDL_AtomicIncRef
+ */
+extern DECLSPEC int SDLCALL SDL_AtomicAdd(SDL_atomic_t *a, int v);
+
+/**
+ * \brief Increment an atomic variable used as a reference count.
+ */
+#ifndef SDL_AtomicIncRef
+#define SDL_AtomicIncRef(a) SDL_AtomicAdd(a, 1)
+#endif
+
+/**
+ * \brief Decrement an atomic variable used as a reference count.
+ *
+ * \return SDL_TRUE if the variable reached zero after decrementing,
+ * SDL_FALSE otherwise
+ */
+#ifndef SDL_AtomicDecRef
+#define SDL_AtomicDecRef(a) (SDL_AtomicAdd(a, -1) == 1)
+#endif
+
+/**
+ * Set a pointer to a new value if it is currently an old value.
+ *
+ * ***Note: If you don't know what this function is for, you shouldn't use
+ * it!***
+ *
+ * \param a a pointer to a pointer.
+ * \param oldval the old pointer value.
+ * \param newval the new pointer value.
+ * \returns SDL_TRUE if the pointer was set, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AtomicCAS
+ * \sa SDL_AtomicGetPtr
+ * \sa SDL_AtomicSetPtr
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_AtomicCASPtr(void **a, void *oldval, void *newval);
+
+/**
+ * Set a pointer to a value atomically.
+ *
+ * ***Note: If you don't know what this function is for, you shouldn't use
+ * it!***
+ *
+ * \param a a pointer to a pointer.
+ * \param v the desired pointer value.
+ * \returns the previous value of the pointer.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_AtomicCASPtr
+ * \sa SDL_AtomicGetPtr
+ */
+extern DECLSPEC void* SDLCALL SDL_AtomicSetPtr(void **a, void* v);
+
+/**
+ * Get the value of a pointer atomically.
+ *
+ * ***Note: If you don't know what this function is for, you shouldn't use
+ * it!***
+ *
+ * \param a a pointer to a pointer.
+ * \returns the current value of a pointer.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_AtomicCASPtr
+ * \sa SDL_AtomicSetPtr
+ */
+extern DECLSPEC void* SDLCALL SDL_AtomicGetPtr(void **a);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+
+#include "close_code.h"
+
+#endif /* SDL_atomic_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_audio.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_audio.h
new file mode 100644
index 00000000..cb76e93e
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_audio.h
@@ -0,0 +1,1502 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* !!! FIXME: several functions in here need Doxygen comments. */
+
+/**
+ * # CategoryAudio
+ *
+ * Access to the raw audio mixing buffer for the SDL library.
+ */
+
+#ifndef SDL_audio_h_
+#define SDL_audio_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+#include "SDL_endian.h"
+#include "SDL_mutex.h"
+#include "SDL_thread.h"
+#include "SDL_rwops.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Audio format flags.
+ *
+ * These are what the 16 bits in SDL_AudioFormat currently mean...
+ * (Unspecified bits are always zero).
+ *
+ * ```
+ * ++-----------------------sample is signed if set
+ * ||
+ * || ++-----------sample is bigendian if set
+ * || ||
+ * || || ++---sample is float if set
+ * || || ||
+ * || || || +---sample bit size---+
+ * || || || | |
+ * 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
+ * ```
+ *
+ * There are macros in SDL 2.0 and later to query these bits.
+ */
+typedef Uint16 SDL_AudioFormat;
+
+/**
+ * \name Audio flags
+ */
+/* @{ */
+
+#define SDL_AUDIO_MASK_BITSIZE (0xFF)
+#define SDL_AUDIO_MASK_DATATYPE (1<<8)
+#define SDL_AUDIO_MASK_ENDIAN (1<<12)
+#define SDL_AUDIO_MASK_SIGNED (1<<15)
+#define SDL_AUDIO_BITSIZE(x) (x & SDL_AUDIO_MASK_BITSIZE)
+#define SDL_AUDIO_ISFLOAT(x) (x & SDL_AUDIO_MASK_DATATYPE)
+#define SDL_AUDIO_ISBIGENDIAN(x) (x & SDL_AUDIO_MASK_ENDIAN)
+#define SDL_AUDIO_ISSIGNED(x) (x & SDL_AUDIO_MASK_SIGNED)
+#define SDL_AUDIO_ISINT(x) (!SDL_AUDIO_ISFLOAT(x))
+#define SDL_AUDIO_ISLITTLEENDIAN(x) (!SDL_AUDIO_ISBIGENDIAN(x))
+#define SDL_AUDIO_ISUNSIGNED(x) (!SDL_AUDIO_ISSIGNED(x))
+
+/**
+ * \name Audio format flags
+ *
+ * Defaults to LSB byte order.
+ */
+/* @{ */
+#define AUDIO_U8 0x0008 /**< Unsigned 8-bit samples */
+#define AUDIO_S8 0x8008 /**< Signed 8-bit samples */
+#define AUDIO_U16LSB 0x0010 /**< Unsigned 16-bit samples */
+#define AUDIO_S16LSB 0x8010 /**< Signed 16-bit samples */
+#define AUDIO_U16MSB 0x1010 /**< As above, but big-endian byte order */
+#define AUDIO_S16MSB 0x9010 /**< As above, but big-endian byte order */
+#define AUDIO_U16 AUDIO_U16LSB
+#define AUDIO_S16 AUDIO_S16LSB
+/* @} */
+
+/**
+ * \name int32 support
+ */
+/* @{ */
+#define AUDIO_S32LSB 0x8020 /**< 32-bit integer samples */
+#define AUDIO_S32MSB 0x9020 /**< As above, but big-endian byte order */
+#define AUDIO_S32 AUDIO_S32LSB
+/* @} */
+
+/**
+ * \name float32 support
+ */
+/* @{ */
+#define AUDIO_F32LSB 0x8120 /**< 32-bit floating point samples */
+#define AUDIO_F32MSB 0x9120 /**< As above, but big-endian byte order */
+#define AUDIO_F32 AUDIO_F32LSB
+/* @} */
+
+/**
+ * \name Native audio byte ordering
+ */
+/* @{ */
+#if SDL_BYTEORDER == SDL_LIL_ENDIAN
+#define AUDIO_U16SYS AUDIO_U16LSB
+#define AUDIO_S16SYS AUDIO_S16LSB
+#define AUDIO_S32SYS AUDIO_S32LSB
+#define AUDIO_F32SYS AUDIO_F32LSB
+#else
+#define AUDIO_U16SYS AUDIO_U16MSB
+#define AUDIO_S16SYS AUDIO_S16MSB
+#define AUDIO_S32SYS AUDIO_S32MSB
+#define AUDIO_F32SYS AUDIO_F32MSB
+#endif
+/* @} */
+
+/**
+ * \name Allow change flags
+ *
+ * Which audio format changes are allowed when opening a device.
+ */
+/* @{ */
+#define SDL_AUDIO_ALLOW_FREQUENCY_CHANGE 0x00000001
+#define SDL_AUDIO_ALLOW_FORMAT_CHANGE 0x00000002
+#define SDL_AUDIO_ALLOW_CHANNELS_CHANGE 0x00000004
+#define SDL_AUDIO_ALLOW_SAMPLES_CHANGE 0x00000008
+#define SDL_AUDIO_ALLOW_ANY_CHANGE (SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_FORMAT_CHANGE|SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)
+/* @} */
+
+/* @} *//* Audio flags */
+
+/**
+ * This function is called when the audio device needs more data.
+ *
+ * \param userdata An application-specific parameter saved in the
+ * SDL_AudioSpec structure.
+ * \param stream A pointer to the audio data buffer.
+ * \param len Length of **stream** in bytes.
+ */
+typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,
+ int len);
+
+/**
+ * The calculated values in this structure are calculated by SDL_OpenAudio().
+ *
+ * For multi-channel audio, the default SDL channel mapping is:
+ *
+ * ```
+ * 2: FL FR (stereo)
+ * 3: FL FR LFE (2.1 surround)
+ * 4: FL FR BL BR (quad)
+ * 5: FL FR LFE BL BR (4.1 surround)
+ * 6: FL FR FC LFE SL SR (5.1 surround - last two can also be BL BR)
+ * 7: FL FR FC LFE BC SL SR (6.1 surround)
+ * 8: FL FR FC LFE BL BR SL SR (7.1 surround)
+ * ```
+ */
+typedef struct SDL_AudioSpec
+{
+ int freq; /**< DSP frequency -- samples per second */
+ SDL_AudioFormat format; /**< Audio data format */
+ Uint8 channels; /**< Number of channels: 1 mono, 2 stereo */
+ Uint8 silence; /**< Audio buffer silence value (calculated) */
+ Uint16 samples; /**< Audio buffer size in sample FRAMES (total samples divided by channel count) */
+ Uint16 padding; /**< Necessary for some compile environments */
+ Uint32 size; /**< Audio buffer size in bytes (calculated) */
+ SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
+ void *userdata; /**< Userdata passed to callback (ignored for NULL callbacks). */
+} SDL_AudioSpec;
+
+
+struct SDL_AudioCVT;
+typedef void (SDLCALL * SDL_AudioFilter) (struct SDL_AudioCVT * cvt,
+ SDL_AudioFormat format);
+
+/**
+ * Upper limit of filters in SDL_AudioCVT
+ *
+ * The maximum number of SDL_AudioFilter functions in SDL_AudioCVT is
+ * currently limited to 9. The SDL_AudioCVT.filters array has 10 pointers, one
+ * of which is the terminating NULL pointer.
+ */
+#define SDL_AUDIOCVT_MAX_FILTERS 9
+
+/**
+ * \struct SDL_AudioCVT
+ * \brief A structure to hold a set of audio conversion filters and buffers.
+ *
+ * Note that various parts of the conversion pipeline can take advantage
+ * of SIMD operations (like SSE2, for example). SDL_AudioCVT doesn't require
+ * you to pass it aligned data, but can possibly run much faster if you
+ * set both its (buf) field to a pointer that is aligned to 16 bytes, and its
+ * (len) field to something that's a multiple of 16, if possible.
+ */
+#if defined(__GNUC__) && !defined(__CHERI_PURE_CAPABILITY__)
+/* This structure is 84 bytes on 32-bit architectures, make sure GCC doesn't
+ pad it out to 88 bytes to guarantee ABI compatibility between compilers.
+ This is not a concern on CHERI architectures, where pointers must be stored
+ at aligned locations otherwise they will become invalid, and thus structs
+ containing pointers cannot be packed without giving a warning or error.
+ vvv
+ The next time we rev the ABI, make sure to size the ints and add padding.
+*/
+#define SDL_AUDIOCVT_PACKED __attribute__((packed))
+#else
+#define SDL_AUDIOCVT_PACKED
+#endif
+/* */
+typedef struct SDL_AudioCVT
+{
+ int needed; /**< Set to 1 if conversion possible */
+ SDL_AudioFormat src_format; /**< Source audio format */
+ SDL_AudioFormat dst_format; /**< Target audio format */
+ double rate_incr; /**< Rate conversion increment */
+ Uint8 *buf; /**< Buffer to hold entire audio data */
+ int len; /**< Length of original audio buffer */
+ int len_cvt; /**< Length of converted audio buffer */
+ int len_mult; /**< buffer must be len*len_mult big */
+ double len_ratio; /**< Given len, final size is len*len_ratio */
+ SDL_AudioFilter filters[SDL_AUDIOCVT_MAX_FILTERS + 1]; /**< NULL-terminated list of filter functions */
+ int filter_index; /**< Current audio conversion function */
+} SDL_AUDIOCVT_PACKED SDL_AudioCVT;
+
+
+/* Function prototypes */
+
+/**
+ * \name Driver discovery functions
+ *
+ * These functions return the list of built in audio drivers, in the
+ * order that they are normally initialized by default.
+ */
+/* @{ */
+
+/**
+ * Use this function to get the number of built-in audio drivers.
+ *
+ * This function returns a hardcoded number. This never returns a negative
+ * value; if there are no drivers compiled into this build of SDL, this
+ * function returns zero. The presence of a driver in this list does not mean
+ * it will function, it just means SDL is capable of interacting with that
+ * interface. For example, a build of SDL might have esound support, but if
+ * there's no esound server available, SDL's esound driver would fail if used.
+ *
+ * By default, SDL tries all drivers, in its preferred order, until one is
+ * found to be usable.
+ *
+ * \returns the number of built-in audio drivers.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetAudioDriver
+ */
+extern DECLSPEC int SDLCALL SDL_GetNumAudioDrivers(void);
+
+/**
+ * Use this function to get the name of a built in audio driver.
+ *
+ * The list of audio drivers is given in the order that they are normally
+ * initialized by default; the drivers that seem more reasonable to choose
+ * first (as far as the SDL developers believe) are earlier in the list.
+ *
+ * The names of drivers are all simple, low-ASCII identifiers, like "alsa",
+ * "coreaudio" or "xaudio2". These never have Unicode characters, and are not
+ * meant to be proper names.
+ *
+ * \param index the index of the audio driver; the value ranges from 0 to
+ * SDL_GetNumAudioDrivers() - 1.
+ * \returns the name of the audio driver at the requested index, or NULL if an
+ * invalid index was specified.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetNumAudioDrivers
+ */
+extern DECLSPEC const char *SDLCALL SDL_GetAudioDriver(int index);
+/* @} */
+
+/**
+ * \name Initialization and cleanup
+ *
+ * \internal These functions are used internally, and should not be used unless
+ * you have a specific need to specify the audio driver you want to
+ * use. You should normally use SDL_Init() or SDL_InitSubSystem().
+ */
+/* @{ */
+
+/**
+ * Use this function to initialize a particular audio driver.
+ *
+ * This function is used internally, and should not be used unless you have a
+ * specific need to designate the audio driver you want to use. You should
+ * normally use SDL_Init() or SDL_InitSubSystem().
+ *
+ * \param driver_name the name of the desired audio driver.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AudioQuit
+ */
+extern DECLSPEC int SDLCALL SDL_AudioInit(const char *driver_name);
+
+/**
+ * Use this function to shut down audio if you initialized it with
+ * SDL_AudioInit().
+ *
+ * This function is used internally, and should not be used unless you have a
+ * specific need to specify the audio driver you want to use. You should
+ * normally use SDL_Quit() or SDL_QuitSubSystem().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AudioInit
+ */
+extern DECLSPEC void SDLCALL SDL_AudioQuit(void);
+/* @} */
+
+/**
+ * Get the name of the current audio driver.
+ *
+ * The returned string points to internal static memory and thus never becomes
+ * invalid, even if you quit the audio subsystem and initialize a new driver
+ * (although such a case would return a different static string from another
+ * call to this function, of course). As such, you should not modify or free
+ * the returned string.
+ *
+ * \returns the name of the current audio driver or NULL if no driver has been
+ * initialized.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AudioInit
+ */
+extern DECLSPEC const char *SDLCALL SDL_GetCurrentAudioDriver(void);
+
+/**
+ * This function is a legacy means of opening the audio device.
+ *
+ * This function remains for compatibility with SDL 1.2, but also because it's
+ * slightly easier to use than the new functions in SDL 2.0. The new, more
+ * powerful, and preferred way to do this is SDL_OpenAudioDevice().
+ *
+ * This function is roughly equivalent to:
+ *
+ * ```c
+ * SDL_OpenAudioDevice(NULL, 0, desired, obtained, SDL_AUDIO_ALLOW_ANY_CHANGE);
+ * ```
+ *
+ * With two notable exceptions:
+ *
+ * - If `obtained` is NULL, we use `desired` (and allow no changes), which
+ * means desired will be modified to have the correct values for silence,
+ * etc, and SDL will convert any differences between your app's specific
+ * request and the hardware behind the scenes.
+ * - The return value is always success or failure, and not a device ID, which
+ * means you can only have one device open at a time with this function.
+ *
+ * \param desired an SDL_AudioSpec structure representing the desired output
+ * format. Please refer to the SDL_OpenAudioDevice
+ * documentation for details on how to prepare this structure.
+ * \param obtained an SDL_AudioSpec structure filled in with the actual
+ * parameters, or NULL.
+ * \returns 0 if successful, placing the actual hardware parameters in the
+ * structure pointed to by `obtained`.
+ *
+ * If `obtained` is NULL, the audio data passed to the callback
+ * function will be guaranteed to be in the requested format, and
+ * will be automatically converted to the actual hardware audio
+ * format if necessary. If `obtained` is NULL, `desired` will have
+ * fields modified.
+ *
+ * This function returns a negative error code on failure to open the
+ * audio device or failure to set up the audio thread; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_CloseAudio
+ * \sa SDL_LockAudio
+ * \sa SDL_PauseAudio
+ * \sa SDL_UnlockAudio
+ */
+extern DECLSPEC int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired,
+ SDL_AudioSpec * obtained);
+
+/**
+ * SDL Audio Device IDs.
+ *
+ * A successful call to SDL_OpenAudio() is always device id 1, and legacy SDL
+ * audio APIs assume you want this device ID. SDL_OpenAudioDevice() calls
+ * always returns devices >= 2 on success. The legacy calls are good both for
+ * backwards compatibility and when you don't care about multiple, specific,
+ * or capture devices.
+ */
+typedef Uint32 SDL_AudioDeviceID;
+
+/**
+ * Get the number of built-in audio devices.
+ *
+ * This function is only valid after successfully initializing the audio
+ * subsystem.
+ *
+ * Note that audio capture support is not implemented as of SDL 2.0.4, so the
+ * `iscapture` parameter is for future expansion and should always be zero for
+ * now.
+ *
+ * This function will return -1 if an explicit list of devices can't be
+ * determined. Returning -1 is not an error. For example, if SDL is set up to
+ * talk to a remote audio server, it can't list every one available on the
+ * Internet, but it will still allow a specific host to be specified in
+ * SDL_OpenAudioDevice().
+ *
+ * In many common cases, when this function returns a value <= 0, it can still
+ * successfully open the default device (NULL for first argument of
+ * SDL_OpenAudioDevice()).
+ *
+ * This function may trigger a complete redetect of available hardware. It
+ * should not be called for each iteration of a loop, but rather once at the
+ * start of a loop:
+ *
+ * ```c
+ * // Don't do this:
+ * for (int i = 0; i < SDL_GetNumAudioDevices(0); i++)
+ *
+ * // do this instead:
+ * const int count = SDL_GetNumAudioDevices(0);
+ * for (int i = 0; i < count; ++i) { do_something_here(); }
+ * ```
+ *
+ * \param iscapture zero to request playback devices, non-zero to request
+ * recording devices.
+ * \returns the number of available devices exposed by the current driver or
+ * -1 if an explicit list of devices can't be determined. A return
+ * value of -1 does not necessarily mean an error condition.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetAudioDeviceName
+ * \sa SDL_OpenAudioDevice
+ */
+extern DECLSPEC int SDLCALL SDL_GetNumAudioDevices(int iscapture);
+
+/**
+ * Get the human-readable name of a specific audio device.
+ *
+ * This function is only valid after successfully initializing the audio
+ * subsystem. The values returned by this function reflect the latest call to
+ * SDL_GetNumAudioDevices(); re-call that function to redetect available
+ * hardware.
+ *
+ * The string returned by this function is UTF-8 encoded, read-only, and
+ * managed internally. You are not to free it. If you need to keep the string
+ * for any length of time, you should make your own copy of it, as it will be
+ * invalid next time any of several other SDL functions are called.
+ *
+ * \param index the index of the audio device; valid values range from 0 to
+ * SDL_GetNumAudioDevices() - 1.
+ * \param iscapture non-zero to query the list of recording devices, zero to
+ * query the list of output devices.
+ * \returns the name of the audio device at the requested index, or NULL on
+ * error.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetNumAudioDevices
+ * \sa SDL_GetDefaultAudioInfo
+ */
+extern DECLSPEC const char *SDLCALL SDL_GetAudioDeviceName(int index,
+ int iscapture);
+
+/**
+ * Get the preferred audio format of a specific audio device.
+ *
+ * This function is only valid after a successfully initializing the audio
+ * subsystem. The values returned by this function reflect the latest call to
+ * SDL_GetNumAudioDevices(); re-call that function to redetect available
+ * hardware.
+ *
+ * `spec` will be filled with the sample rate, sample format, and channel
+ * count.
+ *
+ * \param index the index of the audio device; valid values range from 0 to
+ * SDL_GetNumAudioDevices() - 1.
+ * \param iscapture non-zero to query the list of recording devices, zero to
+ * query the list of output devices.
+ * \param spec The SDL_AudioSpec to be initialized by this function.
+ * \returns 0 on success, nonzero on error.
+ *
+ * \since This function is available since SDL 2.0.16.
+ *
+ * \sa SDL_GetNumAudioDevices
+ * \sa SDL_GetDefaultAudioInfo
+ */
+extern DECLSPEC int SDLCALL SDL_GetAudioDeviceSpec(int index,
+ int iscapture,
+ SDL_AudioSpec *spec);
+
+
+/**
+ * Get the name and preferred format of the default audio device.
+ *
+ * Some (but not all!) platforms have an isolated mechanism to get information
+ * about the "default" device. This can actually be a completely different
+ * device that's not in the list you get from SDL_GetAudioDeviceSpec(). It can
+ * even be a network address! (This is discussed in SDL_OpenAudioDevice().)
+ *
+ * As a result, this call is not guaranteed to be performant, as it can query
+ * the sound server directly every time, unlike the other query functions. You
+ * should call this function sparingly!
+ *
+ * `spec` will be filled with the sample rate, sample format, and channel
+ * count, if a default device exists on the system. If `name` is provided,
+ * will be filled with either a dynamically-allocated UTF-8 string or NULL.
+ *
+ * \param name A pointer to be filled with the name of the default device (can
+ * be NULL). Please call SDL_free() when you are done with this
+ * pointer!
+ * \param spec The SDL_AudioSpec to be initialized by this function.
+ * \param iscapture non-zero to query the default recording device, zero to
+ * query the default output device.
+ * \returns 0 on success, nonzero on error.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GetAudioDeviceName
+ * \sa SDL_GetAudioDeviceSpec
+ * \sa SDL_OpenAudioDevice
+ */
+extern DECLSPEC int SDLCALL SDL_GetDefaultAudioInfo(char **name,
+ SDL_AudioSpec *spec,
+ int iscapture);
+
+
+/**
+ * Open a specific audio device.
+ *
+ * SDL_OpenAudio(), unlike this function, always acts on device ID 1. As such,
+ * this function will never return a 1 so as not to conflict with the legacy
+ * function.
+ *
+ * Please note that SDL 2.0 before 2.0.5 did not support recording; as such,
+ * this function would fail if `iscapture` was not zero. Starting with SDL
+ * 2.0.5, recording is implemented and this value can be non-zero.
+ *
+ * Passing in a `device` name of NULL requests the most reasonable default
+ * (and is equivalent to what SDL_OpenAudio() does to choose a device). The
+ * `device` name is a UTF-8 string reported by SDL_GetAudioDeviceName(), but
+ * some drivers allow arbitrary and driver-specific strings, such as a
+ * hostname/IP address for a remote audio server, or a filename in the
+ * diskaudio driver.
+ *
+ * An opened audio device starts out paused, and should be enabled for playing
+ * by calling SDL_PauseAudioDevice(devid, 0) when you are ready for your audio
+ * callback function to be called. Since the audio driver may modify the
+ * requested size of the audio buffer, you should allocate any local mixing
+ * buffers after you open the audio device.
+ *
+ * The audio callback runs in a separate thread in most cases; you can prevent
+ * race conditions between your callback and other threads without fully
+ * pausing playback with SDL_LockAudioDevice(). For more information about the
+ * callback, see SDL_AudioSpec.
+ *
+ * Managing the audio spec via 'desired' and 'obtained':
+ *
+ * When filling in the desired audio spec structure:
+ *
+ * - `desired->freq` should be the frequency in sample-frames-per-second (Hz).
+ * - `desired->format` should be the audio format (`AUDIO_S16SYS`, etc).
+ * - `desired->samples` is the desired size of the audio buffer, in _sample
+ * frames_ (with stereo output, two samples--left and right--would make a
+ * single sample frame). This number should be a power of two, and may be
+ * adjusted by the audio driver to a value more suitable for the hardware.
+ * Good values seem to range between 512 and 4096 inclusive, depending on
+ * the application and CPU speed. Smaller values reduce latency, but can
+ * lead to underflow if the application is doing heavy processing and cannot
+ * fill the audio buffer in time. Note that the number of sample frames is
+ * directly related to time by the following formula: `ms =
+ * (sampleframes*1000)/freq`
+ * - `desired->size` is the size in _bytes_ of the audio buffer, and is
+ * calculated by SDL_OpenAudioDevice(). You don't initialize this.
+ * - `desired->silence` is the value used to set the buffer to silence, and is
+ * calculated by SDL_OpenAudioDevice(). You don't initialize this.
+ * - `desired->callback` should be set to a function that will be called when
+ * the audio device is ready for more data. It is passed a pointer to the
+ * audio buffer, and the length in bytes of the audio buffer. This function
+ * usually runs in a separate thread, and so you should protect data
+ * structures that it accesses by calling SDL_LockAudioDevice() and
+ * SDL_UnlockAudioDevice() in your code. Alternately, you may pass a NULL
+ * pointer here, and call SDL_QueueAudio() with some frequency, to queue
+ * more audio samples to be played (or for capture devices, call
+ * SDL_DequeueAudio() with some frequency, to obtain audio samples).
+ * - `desired->userdata` is passed as the first parameter to your callback
+ * function. If you passed a NULL callback, this value is ignored.
+ *
+ * `allowed_changes` can have the following flags OR'd together:
+ *
+ * - `SDL_AUDIO_ALLOW_FREQUENCY_CHANGE`
+ * - `SDL_AUDIO_ALLOW_FORMAT_CHANGE`
+ * - `SDL_AUDIO_ALLOW_CHANNELS_CHANGE`
+ * - `SDL_AUDIO_ALLOW_SAMPLES_CHANGE`
+ * - `SDL_AUDIO_ALLOW_ANY_CHANGE`
+ *
+ * These flags specify how SDL should behave when a device cannot offer a
+ * specific feature. If the application requests a feature that the hardware
+ * doesn't offer, SDL will always try to get the closest equivalent.
+ *
+ * For example, if you ask for float32 audio format, but the sound card only
+ * supports int16, SDL will set the hardware to int16. If you had set
+ * SDL_AUDIO_ALLOW_FORMAT_CHANGE, SDL will change the format in the `obtained`
+ * structure. If that flag was *not* set, SDL will prepare to convert your
+ * callback's float32 audio to int16 before feeding it to the hardware and
+ * will keep the originally requested format in the `obtained` structure.
+ *
+ * The resulting audio specs, varying depending on hardware and on what
+ * changes were allowed, will then be written back to `obtained`.
+ *
+ * If your application can only handle one specific data format, pass a zero
+ * for `allowed_changes` and let SDL transparently handle any differences.
+ *
+ * \param device a UTF-8 string reported by SDL_GetAudioDeviceName() or a
+ * driver-specific name as appropriate. NULL requests the most
+ * reasonable default device.
+ * \param iscapture non-zero to specify a device should be opened for
+ * recording, not playback.
+ * \param desired an SDL_AudioSpec structure representing the desired output
+ * format; see SDL_OpenAudio() for more information.
+ * \param obtained an SDL_AudioSpec structure filled in with the actual output
+ * format; see SDL_OpenAudio() for more information.
+ * \param allowed_changes 0, or one or more flags OR'd together.
+ * \returns a valid device ID that is > 0 on success or 0 on failure; call
+ * SDL_GetError() for more information.
+ *
+ * For compatibility with SDL 1.2, this will never return 1, since
+ * SDL reserves that ID for the legacy SDL_OpenAudio() function.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_CloseAudioDevice
+ * \sa SDL_GetAudioDeviceName
+ * \sa SDL_LockAudioDevice
+ * \sa SDL_OpenAudio
+ * \sa SDL_PauseAudioDevice
+ * \sa SDL_UnlockAudioDevice
+ */
+extern DECLSPEC SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(
+ const char *device,
+ int iscapture,
+ const SDL_AudioSpec *desired,
+ SDL_AudioSpec *obtained,
+ int allowed_changes);
+
+
+
+/**
+ * \name Audio state
+ *
+ * Get the current audio state.
+ */
+/* @{ */
+typedef enum
+{
+ SDL_AUDIO_STOPPED = 0,
+ SDL_AUDIO_PLAYING,
+ SDL_AUDIO_PAUSED
+} SDL_AudioStatus;
+
+/**
+ * This function is a legacy means of querying the audio device.
+ *
+ * New programs might want to use SDL_GetAudioDeviceStatus() instead. This
+ * function is equivalent to calling...
+ *
+ * ```c
+ * SDL_GetAudioDeviceStatus(1);
+ * ```
+ *
+ * ...and is only useful if you used the legacy SDL_OpenAudio() function.
+ *
+ * \returns the SDL_AudioStatus of the audio device opened by SDL_OpenAudio().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetAudioDeviceStatus
+ */
+extern DECLSPEC SDL_AudioStatus SDLCALL SDL_GetAudioStatus(void);
+
+/**
+ * Use this function to get the current audio state of an audio device.
+ *
+ * \param dev the ID of an audio device previously opened with
+ * SDL_OpenAudioDevice().
+ * \returns the SDL_AudioStatus of the specified audio device.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PauseAudioDevice
+ */
+extern DECLSPEC SDL_AudioStatus SDLCALL SDL_GetAudioDeviceStatus(SDL_AudioDeviceID dev);
+/* @} *//* Audio State */
+
+/**
+ * \name Pause audio functions
+ *
+ * These functions pause and unpause the audio callback processing.
+ * They should be called with a parameter of 0 after opening the audio
+ * device to start playing sound. This is so you can safely initialize
+ * data for your callback function after opening the audio device.
+ * Silence will be written to the audio device during the pause.
+ */
+/* @{ */
+
+/**
+ * This function is a legacy means of pausing the audio device.
+ *
+ * New programs might want to use SDL_PauseAudioDevice() instead. This
+ * function is equivalent to calling...
+ *
+ * ```c
+ * SDL_PauseAudioDevice(1, pause_on);
+ * ```
+ *
+ * ...and is only useful if you used the legacy SDL_OpenAudio() function.
+ *
+ * \param pause_on non-zero to pause, 0 to unpause.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetAudioStatus
+ * \sa SDL_PauseAudioDevice
+ */
+extern DECLSPEC void SDLCALL SDL_PauseAudio(int pause_on);
+
+/**
+ * Use this function to pause and unpause audio playback on a specified
+ * device.
+ *
+ * This function pauses and unpauses the audio callback processing for a given
+ * device. Newly-opened audio devices start in the paused state, so you must
+ * call this function with **pause_on**=0 after opening the specified audio
+ * device to start playing sound. This allows you to safely initialize data
+ * for your callback function after opening the audio device. Silence will be
+ * written to the audio device while paused, and the audio callback is
+ * guaranteed to not be called. Pausing one device does not prevent other
+ * unpaused devices from running their callbacks.
+ *
+ * Pausing state does not stack; even if you pause a device several times, a
+ * single unpause will start the device playing again, and vice versa. This is
+ * different from how SDL_LockAudioDevice() works.
+ *
+ * If you just need to protect a few variables from race conditions vs your
+ * callback, you shouldn't pause the audio device, as it will lead to dropouts
+ * in the audio playback. Instead, you should use SDL_LockAudioDevice().
+ *
+ * \param dev a device opened by SDL_OpenAudioDevice().
+ * \param pause_on non-zero to pause, 0 to unpause.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LockAudioDevice
+ */
+extern DECLSPEC void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID dev,
+ int pause_on);
+/* @} *//* Pause audio functions */
+
+/**
+ * Load the audio data of a WAVE file into memory.
+ *
+ * Loading a WAVE file requires `src`, `spec`, `audio_buf` and `audio_len` to
+ * be valid pointers. The entire data portion of the file is then loaded into
+ * memory and decoded if necessary.
+ *
+ * If `freesrc` is non-zero, the data source gets automatically closed and
+ * freed before the function returns.
+ *
+ * Supported formats are RIFF WAVE files with the formats PCM (8, 16, 24, and
+ * 32 bits), IEEE Float (32 bits), Microsoft ADPCM and IMA ADPCM (4 bits), and
+ * A-law and mu-law (8 bits). Other formats are currently unsupported and
+ * cause an error.
+ *
+ * If this function succeeds, the pointer returned by it is equal to `spec`
+ * and the pointer to the audio data allocated by the function is written to
+ * `audio_buf` and its length in bytes to `audio_len`. The SDL_AudioSpec
+ * members `freq`, `channels`, and `format` are set to the values of the audio
+ * data in the buffer. The `samples` member is set to a sane default and all
+ * others are set to zero.
+ *
+ * It's necessary to use SDL_FreeWAV() to free the audio data returned in
+ * `audio_buf` when it is no longer used.
+ *
+ * Because of the underspecification of the .WAV format, there are many
+ * problematic files in the wild that cause issues with strict decoders. To
+ * provide compatibility with these files, this decoder is lenient in regards
+ * to the truncation of the file, the fact chunk, and the size of the RIFF
+ * chunk. The hints `SDL_HINT_WAVE_RIFF_CHUNK_SIZE`,
+ * `SDL_HINT_WAVE_TRUNCATION`, and `SDL_HINT_WAVE_FACT_CHUNK` can be used to
+ * tune the behavior of the loading process.
+ *
+ * Any file that is invalid (due to truncation, corruption, or wrong values in
+ * the headers), too big, or unsupported causes an error. Additionally, any
+ * critical I/O error from the data source will terminate the loading process
+ * with an error. The function returns NULL on error and in all cases (with
+ * the exception of `src` being NULL), an appropriate error message will be
+ * set.
+ *
+ * It is required that the data source supports seeking.
+ *
+ * Example:
+ *
+ * ```c
+ * SDL_LoadWAV_RW(SDL_RWFromFile("sample.wav", "rb"), 1, &spec, &buf, &len);
+ * ```
+ *
+ * Note that the SDL_LoadWAV macro does this same thing for you, but in a less
+ * messy way:
+ *
+ * ```c
+ * SDL_LoadWAV("sample.wav", &spec, &buf, &len);
+ * ```
+ *
+ * \param src The data source for the WAVE data.
+ * \param freesrc If non-zero, SDL will _always_ free the data source.
+ * \param spec An SDL_AudioSpec that will be filled in with the wave file's
+ * format details.
+ * \param audio_buf A pointer filled with the audio data, allocated by the
+ * function.
+ * \param audio_len A pointer filled with the length of the audio data buffer
+ * in bytes.
+ * \returns This function, if successfully called, returns `spec`, which will
+ * be filled with the audio data format of the wave source data.
+ * `audio_buf` will be filled with a pointer to an allocated buffer
+ * containing the audio data, and `audio_len` is filled with the
+ * length of that audio buffer in bytes.
+ *
+ * This function returns NULL if the .WAV file cannot be opened, uses
+ * an unknown data format, or is corrupt; call SDL_GetError() for
+ * more information.
+ *
+ * When the application is done with the data returned in
+ * `audio_buf`, it should call SDL_FreeWAV() to dispose of it.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_FreeWAV
+ * \sa SDL_LoadWAV
+ */
+extern DECLSPEC SDL_AudioSpec *SDLCALL SDL_LoadWAV_RW(SDL_RWops * src,
+ int freesrc,
+ SDL_AudioSpec * spec,
+ Uint8 ** audio_buf,
+ Uint32 * audio_len);
+
+/**
+ * Loads a WAV from a file.
+ *
+ * Compatibility convenience function.
+ */
+#define SDL_LoadWAV(file, spec, audio_buf, audio_len) \
+ SDL_LoadWAV_RW(SDL_RWFromFile(file, "rb"),1, spec,audio_buf,audio_len)
+
+/**
+ * Free data previously allocated with SDL_LoadWAV() or SDL_LoadWAV_RW().
+ *
+ * After a WAVE file has been opened with SDL_LoadWAV() or SDL_LoadWAV_RW()
+ * its data can eventually be freed with SDL_FreeWAV(). It is safe to call
+ * this function with a NULL pointer.
+ *
+ * \param audio_buf a pointer to the buffer created by SDL_LoadWAV() or
+ * SDL_LoadWAV_RW().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LoadWAV
+ * \sa SDL_LoadWAV_RW
+ */
+extern DECLSPEC void SDLCALL SDL_FreeWAV(Uint8 * audio_buf);
+
+/**
+ * Initialize an SDL_AudioCVT structure for conversion.
+ *
+ * Before an SDL_AudioCVT structure can be used to convert audio data it must
+ * be initialized with source and destination information.
+ *
+ * This function will zero out every field of the SDL_AudioCVT, so it must be
+ * called before the application fills in the final buffer information.
+ *
+ * Once this function has returned successfully, and reported that a
+ * conversion is necessary, the application fills in the rest of the fields in
+ * SDL_AudioCVT, now that it knows how large a buffer it needs to allocate,
+ * and then can call SDL_ConvertAudio() to complete the conversion.
+ *
+ * \param cvt an SDL_AudioCVT structure filled in with audio conversion
+ * information.
+ * \param src_format the source format of the audio data; for more info see
+ * SDL_AudioFormat.
+ * \param src_channels the number of channels in the source.
+ * \param src_rate the frequency (sample-frames-per-second) of the source.
+ * \param dst_format the destination format of the audio data; for more info
+ * see SDL_AudioFormat.
+ * \param dst_channels the number of channels in the destination.
+ * \param dst_rate the frequency (sample-frames-per-second) of the
+ * destination.
+ * \returns 1 if the audio filter is prepared, 0 if no conversion is needed,
+ * or a negative error code on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_ConvertAudio
+ */
+extern DECLSPEC int SDLCALL SDL_BuildAudioCVT(SDL_AudioCVT * cvt,
+ SDL_AudioFormat src_format,
+ Uint8 src_channels,
+ int src_rate,
+ SDL_AudioFormat dst_format,
+ Uint8 dst_channels,
+ int dst_rate);
+
+/**
+ * Convert audio data to a desired audio format.
+ *
+ * This function does the actual audio data conversion, after the application
+ * has called SDL_BuildAudioCVT() to prepare the conversion information and
+ * then filled in the buffer details.
+ *
+ * Once the application has initialized the `cvt` structure using
+ * SDL_BuildAudioCVT(), allocated an audio buffer and filled it with audio
+ * data in the source format, this function will convert the buffer, in-place,
+ * to the desired format.
+ *
+ * The data conversion may go through several passes; any given pass may
+ * possibly temporarily increase the size of the data. For example, SDL might
+ * expand 16-bit data to 32 bits before resampling to a lower frequency,
+ * shrinking the data size after having grown it briefly. Since the supplied
+ * buffer will be both the source and destination, converting as necessary
+ * in-place, the application must allocate a buffer that will fully contain
+ * the data during its largest conversion pass. After SDL_BuildAudioCVT()
+ * returns, the application should set the `cvt->len` field to the size, in
+ * bytes, of the source data, and allocate a buffer that is `cvt->len *
+ * cvt->len_mult` bytes long for the `buf` field.
+ *
+ * The source data should be copied into this buffer before the call to
+ * SDL_ConvertAudio(). Upon successful return, this buffer will contain the
+ * converted audio, and `cvt->len_cvt` will be the size of the converted data,
+ * in bytes. Any bytes in the buffer past `cvt->len_cvt` are undefined once
+ * this function returns.
+ *
+ * \param cvt an SDL_AudioCVT structure that was previously set up by
+ * SDL_BuildAudioCVT().
+ * \returns 0 if the conversion was completed successfully or a negative error
+ * code on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_BuildAudioCVT
+ */
+extern DECLSPEC int SDLCALL SDL_ConvertAudio(SDL_AudioCVT * cvt);
+
+/* SDL_AudioStream is a new audio conversion interface.
+ The benefits vs SDL_AudioCVT:
+ - it can handle resampling data in chunks without generating
+ artifacts, when it doesn't have the complete buffer available.
+ - it can handle incoming data in any variable size.
+ - You push data as you have it, and pull it when you need it
+ */
+/* this is opaque to the outside world. */
+struct _SDL_AudioStream;
+typedef struct _SDL_AudioStream SDL_AudioStream;
+
+/**
+ * Create a new audio stream.
+ *
+ * \param src_format The format of the source audio.
+ * \param src_channels The number of channels of the source audio.
+ * \param src_rate The sampling rate of the source audio.
+ * \param dst_format The format of the desired audio output.
+ * \param dst_channels The number of channels of the desired audio output.
+ * \param dst_rate The sampling rate of the desired audio output.
+ * \returns 0 on success, or -1 on error.
+ *
+ * \since This function is available since SDL 2.0.7.
+ *
+ * \sa SDL_AudioStreamPut
+ * \sa SDL_AudioStreamGet
+ * \sa SDL_AudioStreamAvailable
+ * \sa SDL_AudioStreamFlush
+ * \sa SDL_AudioStreamClear
+ * \sa SDL_FreeAudioStream
+ */
+extern DECLSPEC SDL_AudioStream * SDLCALL SDL_NewAudioStream(const SDL_AudioFormat src_format,
+ const Uint8 src_channels,
+ const int src_rate,
+ const SDL_AudioFormat dst_format,
+ const Uint8 dst_channels,
+ const int dst_rate);
+
+/**
+ * Add data to be converted/resampled to the stream.
+ *
+ * \param stream The stream the audio data is being added to.
+ * \param buf A pointer to the audio data to add.
+ * \param len The number of bytes to write to the stream.
+ * \returns 0 on success, or -1 on error.
+ *
+ * \since This function is available since SDL 2.0.7.
+ *
+ * \sa SDL_NewAudioStream
+ * \sa SDL_AudioStreamGet
+ * \sa SDL_AudioStreamAvailable
+ * \sa SDL_AudioStreamFlush
+ * \sa SDL_AudioStreamClear
+ * \sa SDL_FreeAudioStream
+ */
+extern DECLSPEC int SDLCALL SDL_AudioStreamPut(SDL_AudioStream *stream, const void *buf, int len);
+
+/**
+ * Get converted/resampled data from the stream
+ *
+ * \param stream The stream the audio is being requested from.
+ * \param buf A buffer to fill with audio data.
+ * \param len The maximum number of bytes to fill.
+ * \returns the number of bytes read from the stream, or -1 on error.
+ *
+ * \since This function is available since SDL 2.0.7.
+ *
+ * \sa SDL_NewAudioStream
+ * \sa SDL_AudioStreamPut
+ * \sa SDL_AudioStreamAvailable
+ * \sa SDL_AudioStreamFlush
+ * \sa SDL_AudioStreamClear
+ * \sa SDL_FreeAudioStream
+ */
+extern DECLSPEC int SDLCALL SDL_AudioStreamGet(SDL_AudioStream *stream, void *buf, int len);
+
+/**
+ * Get the number of converted/resampled bytes available.
+ *
+ * The stream may be buffering data behind the scenes until it has enough to
+ * resample correctly, so this number might be lower than what you expect, or
+ * even be zero. Add more data or flush the stream if you need the data now.
+ *
+ * \since This function is available since SDL 2.0.7.
+ *
+ * \sa SDL_NewAudioStream
+ * \sa SDL_AudioStreamPut
+ * \sa SDL_AudioStreamGet
+ * \sa SDL_AudioStreamFlush
+ * \sa SDL_AudioStreamClear
+ * \sa SDL_FreeAudioStream
+ */
+extern DECLSPEC int SDLCALL SDL_AudioStreamAvailable(SDL_AudioStream *stream);
+
+/**
+ * Tell the stream that you're done sending data, and anything being buffered
+ * should be converted/resampled and made available immediately.
+ *
+ * It is legal to add more data to a stream after flushing, but there will be
+ * audio gaps in the output. Generally this is intended to signal the end of
+ * input, so the complete output becomes available.
+ *
+ * \since This function is available since SDL 2.0.7.
+ *
+ * \sa SDL_NewAudioStream
+ * \sa SDL_AudioStreamPut
+ * \sa SDL_AudioStreamGet
+ * \sa SDL_AudioStreamAvailable
+ * \sa SDL_AudioStreamClear
+ * \sa SDL_FreeAudioStream
+ */
+extern DECLSPEC int SDLCALL SDL_AudioStreamFlush(SDL_AudioStream *stream);
+
+/**
+ * Clear any pending data in the stream without converting it
+ *
+ * \since This function is available since SDL 2.0.7.
+ *
+ * \sa SDL_NewAudioStream
+ * \sa SDL_AudioStreamPut
+ * \sa SDL_AudioStreamGet
+ * \sa SDL_AudioStreamAvailable
+ * \sa SDL_AudioStreamFlush
+ * \sa SDL_FreeAudioStream
+ */
+extern DECLSPEC void SDLCALL SDL_AudioStreamClear(SDL_AudioStream *stream);
+
+/**
+ * Free an audio stream
+ *
+ * \since This function is available since SDL 2.0.7.
+ *
+ * \sa SDL_NewAudioStream
+ * \sa SDL_AudioStreamPut
+ * \sa SDL_AudioStreamGet
+ * \sa SDL_AudioStreamAvailable
+ * \sa SDL_AudioStreamFlush
+ * \sa SDL_AudioStreamClear
+ */
+extern DECLSPEC void SDLCALL SDL_FreeAudioStream(SDL_AudioStream *stream);
+
+/**
+ * Maximum volume allowed in calls to SDL_MixAudio and SDL_MixAudioFormat.
+ */
+#define SDL_MIX_MAXVOLUME 128
+
+/**
+ * This function is a legacy means of mixing audio.
+ *
+ * This function is equivalent to calling...
+ *
+ * ```c
+ * SDL_MixAudioFormat(dst, src, format, len, volume);
+ * ```
+ *
+ * ...where `format` is the obtained format of the audio device from the
+ * legacy SDL_OpenAudio() function.
+ *
+ * \param dst the destination for the mixed audio.
+ * \param src the source audio buffer to be mixed.
+ * \param len the length of the audio buffer in bytes.
+ * \param volume ranges from 0 - 128, and should be set to SDL_MIX_MAXVOLUME
+ * for full audio volume.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_MixAudioFormat
+ */
+extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src,
+ Uint32 len, int volume);
+
+/**
+ * Mix audio data in a specified format.
+ *
+ * This takes an audio buffer `src` of `len` bytes of `format` data and mixes
+ * it into `dst`, performing addition, volume adjustment, and overflow
+ * clipping. The buffer pointed to by `dst` must also be `len` bytes of
+ * `format` data.
+ *
+ * This is provided for convenience -- you can mix your own audio data.
+ *
+ * Do not use this function for mixing together more than two streams of
+ * sample data. The output from repeated application of this function may be
+ * distorted by clipping, because there is no accumulator with greater range
+ * than the input (not to mention this being an inefficient way of doing it).
+ *
+ * It is a common misconception that this function is required to write audio
+ * data to an output stream in an audio callback. While you can do that,
+ * SDL_MixAudioFormat() is really only needed when you're mixing a single
+ * audio stream with a volume adjustment.
+ *
+ * \param dst the destination for the mixed audio.
+ * \param src the source audio buffer to be mixed.
+ * \param format the SDL_AudioFormat structure representing the desired audio
+ * format.
+ * \param len the length of the audio buffer in bytes.
+ * \param volume ranges from 0 - 128, and should be set to SDL_MIX_MAXVOLUME
+ * for full audio volume.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC void SDLCALL SDL_MixAudioFormat(Uint8 * dst,
+ const Uint8 * src,
+ SDL_AudioFormat format,
+ Uint32 len, int volume);
+
+/**
+ * Queue more audio on non-callback devices.
+ *
+ * If you are looking to retrieve queued audio from a non-callback capture
+ * device, you want SDL_DequeueAudio() instead. SDL_QueueAudio() will return
+ * -1 to signify an error if you use it with capture devices.
+ *
+ * SDL offers two ways to feed audio to the device: you can either supply a
+ * callback that SDL triggers with some frequency to obtain more audio (pull
+ * method), or you can supply no callback, and then SDL will expect you to
+ * supply data at regular intervals (push method) with this function.
+ *
+ * There are no limits on the amount of data you can queue, short of
+ * exhaustion of address space. Queued data will drain to the device as
+ * necessary without further intervention from you. If the device needs audio
+ * but there is not enough queued, it will play silence to make up the
+ * difference. This means you will have skips in your audio playback if you
+ * aren't routinely queueing sufficient data.
+ *
+ * This function copies the supplied data, so you are safe to free it when the
+ * function returns. This function is thread-safe, but queueing to the same
+ * device from two threads at once does not promise which buffer will be
+ * queued first.
+ *
+ * You may not queue audio on a device that is using an application-supplied
+ * callback; doing so returns an error. You have to use the audio callback or
+ * queue audio with this function, but not both.
+ *
+ * You should not call SDL_LockAudio() on the device before queueing; SDL
+ * handles locking internally for this function.
+ *
+ * Note that SDL2 does not support planar audio. You will need to resample
+ * from planar audio formats into a non-planar one (see SDL_AudioFormat)
+ * before queuing audio.
+ *
+ * \param dev the device ID to which we will queue audio.
+ * \param data the data to queue to the device for later playback.
+ * \param len the number of bytes (not samples!) to which `data` points.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.4.
+ *
+ * \sa SDL_ClearQueuedAudio
+ * \sa SDL_GetQueuedAudioSize
+ */
+extern DECLSPEC int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID dev, const void *data, Uint32 len);
+
+/**
+ * Dequeue more audio on non-callback devices.
+ *
+ * If you are looking to queue audio for output on a non-callback playback
+ * device, you want SDL_QueueAudio() instead. SDL_DequeueAudio() will always
+ * return 0 if you use it with playback devices.
+ *
+ * SDL offers two ways to retrieve audio from a capture device: you can either
+ * supply a callback that SDL triggers with some frequency as the device
+ * records more audio data, (push method), or you can supply no callback, and
+ * then SDL will expect you to retrieve data at regular intervals (pull
+ * method) with this function.
+ *
+ * There are no limits on the amount of data you can queue, short of
+ * exhaustion of address space. Data from the device will keep queuing as
+ * necessary without further intervention from you. This means you will
+ * eventually run out of memory if you aren't routinely dequeueing data.
+ *
+ * Capture devices will not queue data when paused; if you are expecting to
+ * not need captured audio for some length of time, use SDL_PauseAudioDevice()
+ * to stop the capture device from queueing more data. This can be useful
+ * during, say, level loading times. When unpaused, capture devices will start
+ * queueing data from that point, having flushed any capturable data available
+ * while paused.
+ *
+ * This function is thread-safe, but dequeueing from the same device from two
+ * threads at once does not promise which thread will dequeue data first.
+ *
+ * You may not dequeue audio from a device that is using an
+ * application-supplied callback; doing so returns an error. You have to use
+ * the audio callback, or dequeue audio with this function, but not both.
+ *
+ * You should not call SDL_LockAudio() on the device before dequeueing; SDL
+ * handles locking internally for this function.
+ *
+ * \param dev the device ID from which we will dequeue audio.
+ * \param data a pointer into where audio data should be copied.
+ * \param len the number of bytes (not samples!) to which (data) points.
+ * \returns the number of bytes dequeued, which could be less than requested;
+ * call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.5.
+ *
+ * \sa SDL_ClearQueuedAudio
+ * \sa SDL_GetQueuedAudioSize
+ */
+extern DECLSPEC Uint32 SDLCALL SDL_DequeueAudio(SDL_AudioDeviceID dev, void *data, Uint32 len);
+
+/**
+ * Get the number of bytes of still-queued audio.
+ *
+ * For playback devices: this is the number of bytes that have been queued for
+ * playback with SDL_QueueAudio(), but have not yet been sent to the hardware.
+ *
+ * Once we've sent it to the hardware, this function can not decide the exact
+ * byte boundary of what has been played. It's possible that we just gave the
+ * hardware several kilobytes right before you called this function, but it
+ * hasn't played any of it yet, or maybe half of it, etc.
+ *
+ * For capture devices, this is the number of bytes that have been captured by
+ * the device and are waiting for you to dequeue. This number may grow at any
+ * time, so this only informs of the lower-bound of available data.
+ *
+ * You may not queue or dequeue audio on a device that is using an
+ * application-supplied callback; calling this function on such a device
+ * always returns 0. You have to use the audio callback or queue audio, but
+ * not both.
+ *
+ * You should not call SDL_LockAudio() on the device before querying; SDL
+ * handles locking internally for this function.
+ *
+ * \param dev the device ID of which we will query queued audio size.
+ * \returns the number of bytes (not samples!) of queued audio.
+ *
+ * \since This function is available since SDL 2.0.4.
+ *
+ * \sa SDL_ClearQueuedAudio
+ * \sa SDL_QueueAudio
+ * \sa SDL_DequeueAudio
+ */
+extern DECLSPEC Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID dev);
+
+/**
+ * Drop any queued audio data waiting to be sent to the hardware.
+ *
+ * Immediately after this call, SDL_GetQueuedAudioSize() will return 0. For
+ * output devices, the hardware will start playing silence if more audio isn't
+ * queued. For capture devices, the hardware will start filling the empty
+ * queue with new data if the capture device isn't paused.
+ *
+ * This will not prevent playback of queued audio that's already been sent to
+ * the hardware, as we can not undo that, so expect there to be some fraction
+ * of a second of audio that might still be heard. This can be useful if you
+ * want to, say, drop any pending music or any unprocessed microphone input
+ * during a level change in your game.
+ *
+ * You may not queue or dequeue audio on a device that is using an
+ * application-supplied callback; calling this function on such a device
+ * always returns 0. You have to use the audio callback or queue audio, but
+ * not both.
+ *
+ * You should not call SDL_LockAudio() on the device before clearing the
+ * queue; SDL handles locking internally for this function.
+ *
+ * This function always succeeds and thus returns void.
+ *
+ * \param dev the device ID of which to clear the audio queue.
+ *
+ * \since This function is available since SDL 2.0.4.
+ *
+ * \sa SDL_GetQueuedAudioSize
+ * \sa SDL_QueueAudio
+ * \sa SDL_DequeueAudio
+ */
+extern DECLSPEC void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID dev);
+
+
+/**
+ * \name Audio lock functions
+ *
+ * The lock manipulated by these functions protects the callback function.
+ * During a SDL_LockAudio()/SDL_UnlockAudio() pair, you can be guaranteed that
+ * the callback function is not running. Do not call these from the callback
+ * function or you will cause deadlock.
+ */
+/* @{ */
+
+/**
+ * This function is a legacy means of locking the audio device.
+ *
+ * New programs might want to use SDL_LockAudioDevice() instead. This function
+ * is equivalent to calling...
+ *
+ * ```c
+ * SDL_LockAudioDevice(1);
+ * ```
+ *
+ * ...and is only useful if you used the legacy SDL_OpenAudio() function.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LockAudioDevice
+ * \sa SDL_UnlockAudio
+ * \sa SDL_UnlockAudioDevice
+ */
+extern DECLSPEC void SDLCALL SDL_LockAudio(void);
+
+/**
+ * Use this function to lock out the audio callback function for a specified
+ * device.
+ *
+ * The lock manipulated by these functions protects the audio callback
+ * function specified in SDL_OpenAudioDevice(). During a
+ * SDL_LockAudioDevice()/SDL_UnlockAudioDevice() pair, you can be guaranteed
+ * that the callback function for that device is not running, even if the
+ * device is not paused. While a device is locked, any other unpaused,
+ * unlocked devices may still run their callbacks.
+ *
+ * Calling this function from inside your audio callback is unnecessary. SDL
+ * obtains this lock before calling your function, and releases it when the
+ * function returns.
+ *
+ * You should not hold the lock longer than absolutely necessary. If you hold
+ * it too long, you'll experience dropouts in your audio playback. Ideally,
+ * your application locks the device, sets a few variables and unlocks again.
+ * Do not do heavy work while holding the lock for a device.
+ *
+ * It is safe to lock the audio device multiple times, as long as you unlock
+ * it an equivalent number of times. The callback will not run until the
+ * device has been unlocked completely in this way. If your application fails
+ * to unlock the device appropriately, your callback will never run, you might
+ * hear repeating bursts of audio, and SDL_CloseAudioDevice() will probably
+ * deadlock.
+ *
+ * Internally, the audio device lock is a mutex; if you lock from two threads
+ * at once, not only will you block the audio callback, you'll block the other
+ * thread.
+ *
+ * \param dev the ID of the device to be locked.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_UnlockAudioDevice
+ */
+extern DECLSPEC void SDLCALL SDL_LockAudioDevice(SDL_AudioDeviceID dev);
+
+/**
+ * This function is a legacy means of unlocking the audio device.
+ *
+ * New programs might want to use SDL_UnlockAudioDevice() instead. This
+ * function is equivalent to calling...
+ *
+ * ```c
+ * SDL_UnlockAudioDevice(1);
+ * ```
+ *
+ * ...and is only useful if you used the legacy SDL_OpenAudio() function.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LockAudio
+ * \sa SDL_UnlockAudioDevice
+ */
+extern DECLSPEC void SDLCALL SDL_UnlockAudio(void);
+
+/**
+ * Use this function to unlock the audio callback function for a specified
+ * device.
+ *
+ * This function should be paired with a previous SDL_LockAudioDevice() call.
+ *
+ * \param dev the ID of the device to be unlocked.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LockAudioDevice
+ */
+extern DECLSPEC void SDLCALL SDL_UnlockAudioDevice(SDL_AudioDeviceID dev);
+/* @} *//* Audio lock functions */
+
+/**
+ * This function is a legacy means of closing the audio device.
+ *
+ * This function is equivalent to calling...
+ *
+ * ```c
+ * SDL_CloseAudioDevice(1);
+ * ```
+ *
+ * ...and is only useful if you used the legacy SDL_OpenAudio() function.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_OpenAudio
+ */
+extern DECLSPEC void SDLCALL SDL_CloseAudio(void);
+
+/**
+ * Use this function to shut down audio processing and close the audio device.
+ *
+ * The application should close open audio devices once they are no longer
+ * needed. Calling this function will wait until the device's audio callback
+ * is not running, release the audio hardware and then clean up internal
+ * state. No further audio will play from this device once this function
+ * returns.
+ *
+ * This function may block briefly while pending audio data is played by the
+ * hardware, so that applications don't drop the last buffer of data they
+ * supplied.
+ *
+ * The device ID is invalid as soon as the device is closed, and is eligible
+ * for reuse in a new SDL_OpenAudioDevice() call immediately.
+ *
+ * \param dev an audio device previously opened with SDL_OpenAudioDevice().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_OpenAudioDevice
+ */
+extern DECLSPEC void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID dev);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_audio_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_bits.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_bits.h
new file mode 100644
index 00000000..747f5565
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_bits.h
@@ -0,0 +1,132 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryBits
+ *
+ * Functions for fiddling with bits and bitmasks.
+ */
+
+#ifndef SDL_bits_h_
+#define SDL_bits_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \file SDL_bits.h
+ */
+
+/**
+ * Get the index of the most significant bit. Result is undefined when called
+ * with 0. This operation can also be stated as "count leading zeroes" and
+ * "log base 2".
+ *
+ * \return the index of the most significant bit, or -1 if the value is 0.
+ */
+#if defined(__WATCOMC__) && defined(__386__)
+extern __inline int _SDL_bsr_watcom(Uint32);
+#pragma aux _SDL_bsr_watcom = \
+ "bsr eax, eax" \
+ parm [eax] nomemory \
+ value [eax] \
+ modify exact [eax] nomemory;
+#endif
+
+/**
+ * Use this function to get the index of the most significant (set) bit in a
+ *
+ * \param x the number to find the MSB of.
+ * \returns the index of the most significant bit of x, or -1 if x is 0.
+ */
+SDL_FORCE_INLINE int
+SDL_MostSignificantBitIndex32(Uint32 x)
+{
+#if defined(__GNUC__) && (__GNUC__ >= 4 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4))
+ /* Count Leading Zeroes builtin in GCC.
+ * http://gcc.gnu.org/onlinedocs/gcc-4.3.4/gcc/Other-Builtins.html
+ */
+ if (x == 0) {
+ return -1;
+ }
+ return 31 - __builtin_clz(x);
+#elif defined(__WATCOMC__) && defined(__386__)
+ if (x == 0) {
+ return -1;
+ }
+ return _SDL_bsr_watcom(x);
+#elif defined(_MSC_VER)
+ unsigned long index;
+ if (_BitScanReverse(&index, x)) {
+ return index;
+ }
+ return -1;
+#else
+ /* Based off of Bit Twiddling Hacks by Sean Eron Anderson
+ * , released in the public domain.
+ * http://graphics.stanford.edu/~seander/bithacks.html#IntegerLog
+ */
+ const Uint32 b[] = {0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000};
+ const int S[] = {1, 2, 4, 8, 16};
+
+ int msbIndex = 0;
+ int i;
+
+ if (x == 0) {
+ return -1;
+ }
+
+ for (i = 4; i >= 0; i--)
+ {
+ if (x & b[i])
+ {
+ x >>= S[i];
+ msbIndex |= S[i];
+ }
+ }
+
+ return msbIndex;
+#endif
+}
+
+SDL_FORCE_INLINE SDL_bool
+SDL_HasExactlyOneBitSet32(Uint32 x)
+{
+ if (x && !(x & (x - 1))) {
+ return SDL_TRUE;
+ }
+ return SDL_FALSE;
+}
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_bits_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_blendmode.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_blendmode.h
new file mode 100644
index 00000000..c0c68113
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_blendmode.h
@@ -0,0 +1,199 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryBlendmode
+ *
+ * Header file declaring the SDL_BlendMode enumeration
+ */
+
+#ifndef SDL_blendmode_h_
+#define SDL_blendmode_h_
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The blend mode used in SDL_RenderCopy() and drawing operations.
+ */
+typedef enum SDL_BlendMode
+{
+ SDL_BLENDMODE_NONE = 0x00000000, /**< no blending
+ dstRGBA = srcRGBA */
+ SDL_BLENDMODE_BLEND = 0x00000001, /**< alpha blending
+ dstRGB = (srcRGB * srcA) + (dstRGB * (1-srcA))
+ dstA = srcA + (dstA * (1-srcA)) */
+ SDL_BLENDMODE_ADD = 0x00000002, /**< additive blending
+ dstRGB = (srcRGB * srcA) + dstRGB
+ dstA = dstA */
+ SDL_BLENDMODE_MOD = 0x00000004, /**< color modulate
+ dstRGB = srcRGB * dstRGB
+ dstA = dstA */
+ SDL_BLENDMODE_MUL = 0x00000008, /**< color multiply
+ dstRGB = (srcRGB * dstRGB) + (dstRGB * (1-srcA))
+ dstA = dstA */
+ SDL_BLENDMODE_INVALID = 0x7FFFFFFF
+
+ /* Additional custom blend modes can be returned by SDL_ComposeCustomBlendMode() */
+
+} SDL_BlendMode;
+
+/**
+ * The blend operation used when combining source and destination pixel
+ * components
+ */
+typedef enum SDL_BlendOperation
+{
+ SDL_BLENDOPERATION_ADD = 0x1, /**< dst + src: supported by all renderers */
+ SDL_BLENDOPERATION_SUBTRACT = 0x2, /**< src - dst : supported by D3D9, D3D11, OpenGL, OpenGLES */
+ SDL_BLENDOPERATION_REV_SUBTRACT = 0x3, /**< dst - src : supported by D3D9, D3D11, OpenGL, OpenGLES */
+ SDL_BLENDOPERATION_MINIMUM = 0x4, /**< min(dst, src) : supported by D3D9, D3D11 */
+ SDL_BLENDOPERATION_MAXIMUM = 0x5 /**< max(dst, src) : supported by D3D9, D3D11 */
+} SDL_BlendOperation;
+
+/**
+ * The normalized factor used to multiply pixel components
+ */
+typedef enum SDL_BlendFactor
+{
+ SDL_BLENDFACTOR_ZERO = 0x1, /**< 0, 0, 0, 0 */
+ SDL_BLENDFACTOR_ONE = 0x2, /**< 1, 1, 1, 1 */
+ SDL_BLENDFACTOR_SRC_COLOR = 0x3, /**< srcR, srcG, srcB, srcA */
+ SDL_BLENDFACTOR_ONE_MINUS_SRC_COLOR = 0x4, /**< 1-srcR, 1-srcG, 1-srcB, 1-srcA */
+ SDL_BLENDFACTOR_SRC_ALPHA = 0x5, /**< srcA, srcA, srcA, srcA */
+ SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA = 0x6, /**< 1-srcA, 1-srcA, 1-srcA, 1-srcA */
+ SDL_BLENDFACTOR_DST_COLOR = 0x7, /**< dstR, dstG, dstB, dstA */
+ SDL_BLENDFACTOR_ONE_MINUS_DST_COLOR = 0x8, /**< 1-dstR, 1-dstG, 1-dstB, 1-dstA */
+ SDL_BLENDFACTOR_DST_ALPHA = 0x9, /**< dstA, dstA, dstA, dstA */
+ SDL_BLENDFACTOR_ONE_MINUS_DST_ALPHA = 0xA /**< 1-dstA, 1-dstA, 1-dstA, 1-dstA */
+} SDL_BlendFactor;
+
+/**
+ * Compose a custom blend mode for renderers.
+ *
+ * The functions SDL_SetRenderDrawBlendMode and SDL_SetTextureBlendMode accept
+ * the SDL_BlendMode returned by this function if the renderer supports it.
+ *
+ * A blend mode controls how the pixels from a drawing operation (source) get
+ * combined with the pixels from the render target (destination). First, the
+ * components of the source and destination pixels get multiplied with their
+ * blend factors. Then, the blend operation takes the two products and
+ * calculates the result that will get stored in the render target.
+ *
+ * Expressed in pseudocode, it would look like this:
+ *
+ * ```c
+ * dstRGB = colorOperation(srcRGB * srcColorFactor, dstRGB * dstColorFactor);
+ * dstA = alphaOperation(srcA * srcAlphaFactor, dstA * dstAlphaFactor);
+ * ```
+ *
+ * Where the functions `colorOperation(src, dst)` and `alphaOperation(src,
+ * dst)` can return one of the following:
+ *
+ * - `src + dst`
+ * - `src - dst`
+ * - `dst - src`
+ * - `min(src, dst)`
+ * - `max(src, dst)`
+ *
+ * The red, green, and blue components are always multiplied with the first,
+ * second, and third components of the SDL_BlendFactor, respectively. The
+ * fourth component is not used.
+ *
+ * The alpha component is always multiplied with the fourth component of the
+ * SDL_BlendFactor. The other components are not used in the alpha
+ * calculation.
+ *
+ * Support for these blend modes varies for each renderer. To check if a
+ * specific SDL_BlendMode is supported, create a renderer and pass it to
+ * either SDL_SetRenderDrawBlendMode or SDL_SetTextureBlendMode. They will
+ * return with an error if the blend mode is not supported.
+ *
+ * This list describes the support of custom blend modes for each renderer in
+ * SDL 2.0.6. All renderers support the four blend modes listed in the
+ * SDL_BlendMode enumeration.
+ *
+ * - **direct3d**: Supports all operations with all factors. However, some
+ * factors produce unexpected results with `SDL_BLENDOPERATION_MINIMUM` and
+ * `SDL_BLENDOPERATION_MAXIMUM`.
+ * - **direct3d11**: Same as Direct3D 9.
+ * - **opengl**: Supports the `SDL_BLENDOPERATION_ADD` operation with all
+ * factors. OpenGL versions 1.1, 1.2, and 1.3 do not work correctly with SDL
+ * 2.0.6.
+ * - **opengles**: Supports the `SDL_BLENDOPERATION_ADD` operation with all
+ * factors. Color and alpha factors need to be the same. OpenGL ES 1
+ * implementation specific: May also support `SDL_BLENDOPERATION_SUBTRACT`
+ * and `SDL_BLENDOPERATION_REV_SUBTRACT`. May support color and alpha
+ * operations being different from each other. May support color and alpha
+ * factors being different from each other.
+ * - **opengles2**: Supports the `SDL_BLENDOPERATION_ADD`,
+ * `SDL_BLENDOPERATION_SUBTRACT`, `SDL_BLENDOPERATION_REV_SUBTRACT`
+ * operations with all factors.
+ * - **psp**: No custom blend mode support.
+ * - **software**: No custom blend mode support.
+ *
+ * Some renderers do not provide an alpha component for the default render
+ * target. The `SDL_BLENDFACTOR_DST_ALPHA` and
+ * `SDL_BLENDFACTOR_ONE_MINUS_DST_ALPHA` factors do not have an effect in this
+ * case.
+ *
+ * \param srcColorFactor the SDL_BlendFactor applied to the red, green, and
+ * blue components of the source pixels.
+ * \param dstColorFactor the SDL_BlendFactor applied to the red, green, and
+ * blue components of the destination pixels.
+ * \param colorOperation the SDL_BlendOperation used to combine the red,
+ * green, and blue components of the source and
+ * destination pixels.
+ * \param srcAlphaFactor the SDL_BlendFactor applied to the alpha component of
+ * the source pixels.
+ * \param dstAlphaFactor the SDL_BlendFactor applied to the alpha component of
+ * the destination pixels.
+ * \param alphaOperation the SDL_BlendOperation used to combine the alpha
+ * component of the source and destination pixels.
+ * \returns an SDL_BlendMode that represents the chosen factors and
+ * operations.
+ *
+ * \since This function is available since SDL 2.0.6.
+ *
+ * \sa SDL_SetRenderDrawBlendMode
+ * \sa SDL_GetRenderDrawBlendMode
+ * \sa SDL_SetTextureBlendMode
+ * \sa SDL_GetTextureBlendMode
+ */
+extern DECLSPEC SDL_BlendMode SDLCALL SDL_ComposeCustomBlendMode(SDL_BlendFactor srcColorFactor,
+ SDL_BlendFactor dstColorFactor,
+ SDL_BlendOperation colorOperation,
+ SDL_BlendFactor srcAlphaFactor,
+ SDL_BlendFactor dstAlphaFactor,
+ SDL_BlendOperation alphaOperation);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_blendmode_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_clipboard.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_clipboard.h
new file mode 100644
index 00000000..2ae16a1d
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_clipboard.h
@@ -0,0 +1,141 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryClipboard
+ *
+ * Include file for SDL clipboard handling
+ */
+
+#ifndef SDL_clipboard_h_
+#define SDL_clipboard_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Function prototypes */
+
+/**
+ * Put UTF-8 text into the clipboard.
+ *
+ * \param text the text to store in the clipboard.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetClipboardText
+ * \sa SDL_HasClipboardText
+ */
+extern DECLSPEC int SDLCALL SDL_SetClipboardText(const char *text);
+
+/**
+ * Get UTF-8 text from the clipboard, which must be freed with SDL_free().
+ *
+ * This functions returns empty string if there was not enough memory left for
+ * a copy of the clipboard's content.
+ *
+ * \returns the clipboard text on success or an empty string on failure; call
+ * SDL_GetError() for more information. Caller must call SDL_free()
+ * on the returned pointer when done with it (even if there was an
+ * error).
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HasClipboardText
+ * \sa SDL_SetClipboardText
+ */
+extern DECLSPEC char * SDLCALL SDL_GetClipboardText(void);
+
+/**
+ * Query whether the clipboard exists and contains a non-empty text string.
+ *
+ * \returns SDL_TRUE if the clipboard has text, or SDL_FALSE if it does not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetClipboardText
+ * \sa SDL_SetClipboardText
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasClipboardText(void);
+
+/**
+ * Put UTF-8 text into the primary selection.
+ *
+ * \param text the text to store in the primary selection.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.26.0.
+ *
+ * \sa SDL_GetPrimarySelectionText
+ * \sa SDL_HasPrimarySelectionText
+ */
+extern DECLSPEC int SDLCALL SDL_SetPrimarySelectionText(const char *text);
+
+/**
+ * Get UTF-8 text from the primary selection, which must be freed with
+ * SDL_free().
+ *
+ * This functions returns empty string if there was not enough memory left for
+ * a copy of the primary selection's content.
+ *
+ * \returns the primary selection text on success or an empty string on
+ * failure; call SDL_GetError() for more information. Caller must
+ * call SDL_free() on the returned pointer when done with it (even if
+ * there was an error).
+ *
+ * \since This function is available since SDL 2.26.0.
+ *
+ * \sa SDL_HasPrimarySelectionText
+ * \sa SDL_SetPrimarySelectionText
+ */
+extern DECLSPEC char * SDLCALL SDL_GetPrimarySelectionText(void);
+
+/**
+ * Query whether the primary selection exists and contains a non-empty text
+ * string.
+ *
+ * \returns SDL_TRUE if the primary selection has text, or SDL_FALSE if it
+ * does not.
+ *
+ * \since This function is available since SDL 2.26.0.
+ *
+ * \sa SDL_GetPrimarySelectionText
+ * \sa SDL_SetPrimarySelectionText
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasPrimarySelectionText(void);
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_clipboard_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_config.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_config.h
new file mode 100644
index 00000000..35088ffa
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_config.h
@@ -0,0 +1,571 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_config_h_
+#define SDL_config_h_
+
+/**
+ * \file SDL_config.h.in
+ *
+ * This is a set of defines to configure the SDL features
+ */
+
+/* General platform specific identifiers */
+#include "SDL_platform.h"
+
+/* C language features */
+/* #undef const */
+/* #undef inline */
+/* #undef volatile */
+
+/* C datatypes */
+/* Define SIZEOF_VOIDP for 64/32 architectures */
+#if defined(__LP64__) || defined(_LP64) || defined(_WIN64)
+#define SIZEOF_VOIDP 8
+#else
+#define SIZEOF_VOIDP 4
+#endif
+
+/* #undef HAVE_GCC_ATOMICS */
+/* #undef HAVE_GCC_SYNC_LOCK_TEST_AND_SET */
+
+/* Comment this if you want to build without any C library requirements */
+/* #undef HAVE_LIBC */
+#ifdef HAVE_LIBC
+
+/* Useful headers */
+/* #undef STDC_HEADERS */
+/* #undef HAVE_ALLOCA_H */
+/* #undef HAVE_CTYPE_H */
+/* #undef HAVE_FLOAT_H */
+/* #undef HAVE_ICONV_H */
+/* #undef HAVE_INTTYPES_H */
+/* #undef HAVE_LIMITS_H */
+/* #undef HAVE_MALLOC_H */
+/* #undef HAVE_MATH_H */
+/* #undef HAVE_MEMORY_H */
+/* #undef HAVE_SIGNAL_H */
+#define HAVE_STDARG_H 1
+#define HAVE_STDDEF_H 1
+#define HAVE_STDINT_H 1
+/* #undef HAVE_STDIO_H */
+/* #undef HAVE_STDLIB_H */
+/* #undef HAVE_STRINGS_H */
+/* #undef HAVE_STRING_H */
+/* #undef HAVE_SYS_TYPES_H */
+/* #undef HAVE_WCHAR_H */
+/* #undef HAVE_LINUX_INPUT_H */
+/* #undef HAVE_PTHREAD_NP_H */
+/* #undef HAVE_LIBUNWIND_H */
+
+/* C library functions */
+/* #undef HAVE_DLOPEN */
+/* #undef HAVE_MALLOC */
+/* #undef HAVE_CALLOC */
+/* #undef HAVE_REALLOC */
+/* #undef HAVE_FREE */
+/* #undef HAVE_ALLOCA */
+#ifndef __WIN32__ /* Don't use C runtime versions of these on Windows */
+/* #undef HAVE_GETENV */
+/* #undef HAVE_SETENV */
+/* #undef HAVE_PUTENV */
+/* #undef HAVE_UNSETENV */
+#endif
+/* #undef HAVE_QSORT */
+/* #undef HAVE_BSEARCH */
+/* #undef HAVE_ABS */
+/* #undef HAVE_BCOPY */
+/* #undef HAVE_MEMSET */
+/* #undef HAVE_MEMCPY */
+/* #undef HAVE_MEMMOVE */
+/* #undef HAVE_MEMCMP */
+/* #undef HAVE_WCSLEN */
+/* #undef HAVE_WCSLCPY */
+/* #undef HAVE_WCSLCAT */
+/* #undef HAVE__WCSDUP */
+/* #undef HAVE_WCSDUP */
+/* #undef HAVE_WCSSTR */
+/* #undef HAVE_WCSCMP */
+/* #undef HAVE_WCSNCMP */
+/* #undef HAVE_WCSCASECMP */
+/* #undef HAVE__WCSICMP */
+/* #undef HAVE_WCSNCASECMP */
+/* #undef HAVE__WCSNICMP */
+/* #undef HAVE_STRLEN */
+/* #undef HAVE_STRLCPY */
+/* #undef HAVE_STRLCAT */
+/* #undef HAVE__STRREV */
+/* #undef HAVE__STRUPR */
+/* #undef HAVE__STRLWR */
+/* #undef HAVE_INDEX */
+/* #undef HAVE_RINDEX */
+/* #undef HAVE_STRCHR */
+/* #undef HAVE_STRRCHR */
+/* #undef HAVE_STRSTR */
+/* #undef HAVE_STRTOK_R */
+/* #undef HAVE_ITOA */
+/* #undef HAVE__LTOA */
+/* #undef HAVE__UITOA */
+/* #undef HAVE__ULTOA */
+/* #undef HAVE_STRTOL */
+/* #undef HAVE_STRTOUL */
+/* #undef HAVE__I64TOA */
+/* #undef HAVE__UI64TOA */
+/* #undef HAVE_STRTOLL */
+/* #undef HAVE_STRTOULL */
+/* #undef HAVE_STRTOD */
+/* #undef HAVE_ATOI */
+/* #undef HAVE_ATOF */
+/* #undef HAVE_STRCMP */
+/* #undef HAVE_STRNCMP */
+/* #undef HAVE__STRICMP */
+/* #undef HAVE_STRCASECMP */
+/* #undef HAVE__STRNICMP */
+/* #undef HAVE_STRNCASECMP */
+/* #undef HAVE_STRCASESTR */
+/* #undef HAVE_SSCANF */
+/* #undef HAVE_VSSCANF */
+/* #undef HAVE_VSNPRINTF */
+/* #undef HAVE_M_PI */
+/* #undef HAVE_ACOS */
+/* #undef HAVE_ACOSF */
+/* #undef HAVE_ASIN */
+/* #undef HAVE_ASINF */
+/* #undef HAVE_ATAN */
+/* #undef HAVE_ATANF */
+/* #undef HAVE_ATAN2 */
+/* #undef HAVE_ATAN2F */
+/* #undef HAVE_CEIL */
+/* #undef HAVE_CEILF */
+/* #undef HAVE_COPYSIGN */
+/* #undef HAVE_COPYSIGNF */
+/* #undef HAVE_COS */
+/* #undef HAVE_COSF */
+/* #undef HAVE_EXP */
+/* #undef HAVE_EXPF */
+/* #undef HAVE_FABS */
+/* #undef HAVE_FABSF */
+/* #undef HAVE_FLOOR */
+/* #undef HAVE_FLOORF */
+/* #undef HAVE_FMOD */
+/* #undef HAVE_FMODF */
+/* #undef HAVE_LOG */
+/* #undef HAVE_LOGF */
+/* #undef HAVE_LOG10 */
+/* #undef HAVE_LOG10F */
+/* #undef HAVE_LROUND */
+/* #undef HAVE_LROUNDF */
+/* #undef HAVE_POW */
+/* #undef HAVE_POWF */
+/* #undef HAVE_ROUND */
+/* #undef HAVE_ROUNDF */
+/* #undef HAVE_SCALBN */
+/* #undef HAVE_SCALBNF */
+/* #undef HAVE_SIN */
+/* #undef HAVE_SINF */
+/* #undef HAVE_SQRT */
+/* #undef HAVE_SQRTF */
+/* #undef HAVE_TAN */
+/* #undef HAVE_TANF */
+/* #undef HAVE_TRUNC */
+/* #undef HAVE_TRUNCF */
+/* #undef HAVE_FOPEN64 */
+/* #undef HAVE_FSEEKO */
+/* #undef HAVE_FSEEKO64 */
+/* #undef HAVE_MEMFD_CREATE */
+/* #undef HAVE_POSIX_FALLOCATE */
+/* #undef HAVE_SIGACTION */
+/* #undef HAVE_SIGTIMEDWAIT */
+/* #undef HAVE_SA_SIGACTION */
+/* #undef HAVE_SETJMP */
+/* #undef HAVE_NANOSLEEP */
+/* #undef HAVE_SYSCONF */
+/* #undef HAVE_SYSCTLBYNAME */
+/* #undef HAVE_CLOCK_GETTIME */
+/* #undef HAVE_GETPAGESIZE */
+/* #undef HAVE_MPROTECT */
+/* #undef HAVE_ICONV */
+/* #undef SDL_USE_LIBICONV */
+/* #undef HAVE_PTHREAD_SETNAME_NP */
+/* #undef HAVE_PTHREAD_SET_NAME_NP */
+/* #undef HAVE_SEM_TIMEDWAIT */
+/* #undef HAVE_GETAUXVAL */
+/* #undef HAVE_ELF_AUX_INFO */
+/* #undef HAVE_POLL */
+/* #undef HAVE__EXIT */
+
+#else
+#define HAVE_STDARG_H 1
+#define HAVE_STDDEF_H 1
+#define HAVE_STDINT_H 1
+/* #undef HAVE_FLOAT_H */
+#endif /* HAVE_LIBC */
+
+/* #undef HAVE_ALTIVEC_H */
+/* #undef HAVE_DBUS_DBUS_H */
+/* #undef HAVE_FCITX */
+/* #undef HAVE_IBUS_IBUS_H */
+/* #undef HAVE_SYS_INOTIFY_H */
+/* #undef HAVE_INOTIFY_INIT */
+/* #undef HAVE_INOTIFY_INIT1 */
+/* #undef HAVE_INOTIFY */
+/* #undef HAVE_LIBUSB */
+/* #undef HAVE_O_CLOEXEC */
+
+/* Apple platforms might be building universal binaries, where Intel builds
+ can use immintrin.h but other architectures can't. */
+#ifdef __APPLE__
+# if defined(__has_include) && (defined(__i386__) || defined(__x86_64))
+# if __has_include()
+# define HAVE_IMMINTRIN_H 1
+# endif
+# endif
+#else /* non-Apple platforms can use the normal CMake check for this. */
+#define HAVE_IMMINTRIN_H 1
+#endif
+
+/* #undef HAVE_LIBUDEV_H */
+/* #undef HAVE_LIBSAMPLERATE_H */
+/* #undef HAVE_LIBDECOR_H */
+
+#define HAVE_D3D_H 1
+#define HAVE_D3D11_H 1
+#define HAVE_D3D12_H 1
+#define HAVE_DDRAW_H 1
+#define HAVE_DSOUND_H 1
+/* #undef HAVE_DINPUT_H */
+#define HAVE_XINPUT_H 1
+#define HAVE_WINDOWS_GAMING_INPUT_H 1
+#define HAVE_DXGI_H 1
+
+#define HAVE_MMDEVICEAPI_H 1
+#define HAVE_AUDIOCLIENT_H 1
+#define HAVE_TPCSHRD_H 1
+#define HAVE_SENSORSAPI_H 1
+#define HAVE_ROAPI_H 1
+#define HAVE_SHELLSCALINGAPI_H 1
+
+/* #undef USE_POSIX_SPAWN */
+
+/* SDL internal assertion support */
+#if 0
+/* #undef SDL_DEFAULT_ASSERT_LEVEL */
+#endif
+
+/* Allow disabling of core subsystems */
+/* #undef SDL_ATOMIC_DISABLED */
+/* #undef SDL_AUDIO_DISABLED */
+/* #undef SDL_CPUINFO_DISABLED */
+/* #undef SDL_EVENTS_DISABLED */
+/* #undef SDL_FILE_DISABLED */
+/* #undef SDL_JOYSTICK_DISABLED */
+/* #undef SDL_HAPTIC_DISABLED */
+/* #undef SDL_HIDAPI_DISABLED */
+/* #undef SDL_SENSOR_DISABLED */
+/* #undef SDL_LOADSO_DISABLED */
+/* #undef SDL_RENDER_DISABLED */
+/* #undef SDL_THREADS_DISABLED */
+/* #undef SDL_TIMERS_DISABLED */
+/* #undef SDL_VIDEO_DISABLED */
+/* #undef SDL_POWER_DISABLED */
+/* #undef SDL_FILESYSTEM_DISABLED */
+/* #undef SDL_LOCALE_DISABLED */
+/* #undef SDL_MISC_DISABLED */
+
+/* Enable various audio drivers */
+/* #undef SDL_AUDIO_DRIVER_ALSA */
+/* #undef SDL_AUDIO_DRIVER_ALSA_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_ANDROID */
+/* #undef SDL_AUDIO_DRIVER_OPENSLES */
+/* #undef SDL_AUDIO_DRIVER_AAUDIO */
+/* #undef SDL_AUDIO_DRIVER_ARTS */
+/* #undef SDL_AUDIO_DRIVER_ARTS_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_COREAUDIO */
+#define SDL_AUDIO_DRIVER_DISK 1
+/* #undef SDL_AUDIO_DRIVER_DSOUND */
+#define SDL_AUDIO_DRIVER_DUMMY 1
+/* #undef SDL_AUDIO_DRIVER_EMSCRIPTEN */
+/* #undef SDL_AUDIO_DRIVER_ESD */
+/* #undef SDL_AUDIO_DRIVER_ESD_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_FUSIONSOUND */
+/* #undef SDL_AUDIO_DRIVER_FUSIONSOUND_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_HAIKU */
+/* #undef SDL_AUDIO_DRIVER_JACK */
+/* #undef SDL_AUDIO_DRIVER_JACK_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_NAS */
+/* #undef SDL_AUDIO_DRIVER_NAS_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_NETBSD */
+/* #undef SDL_AUDIO_DRIVER_OSS */
+/* #undef SDL_AUDIO_DRIVER_PAUDIO */
+/* #undef SDL_AUDIO_DRIVER_PIPEWIRE */
+/* #undef SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_PULSEAUDIO */
+/* #undef SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_QSA */
+/* #undef SDL_AUDIO_DRIVER_SNDIO */
+/* #undef SDL_AUDIO_DRIVER_SNDIO_DYNAMIC */
+/* #undef SDL_AUDIO_DRIVER_SUNAUDIO */
+#define SDL_AUDIO_DRIVER_WASAPI 1
+/* #undef SDL_AUDIO_DRIVER_WINMM */
+/* #undef SDL_AUDIO_DRIVER_OS2 */
+/* #undef SDL_AUDIO_DRIVER_VITA */
+/* #undef SDL_AUDIO_DRIVER_PSP */
+/* #undef SDL_AUDIO_DRIVER_PS2 */
+/* #undef SDL_AUDIO_DRIVER_N3DS */
+
+/* Enable various input drivers */
+/* #undef SDL_INPUT_LINUXEV */
+/* #undef SDL_INPUT_LINUXKD */
+/* #undef SDL_INPUT_FBSDKBIO */
+/* #undef SDL_INPUT_WSCONS */
+/* #undef SDL_JOYSTICK_ANDROID */
+/* #undef SDL_JOYSTICK_HAIKU */
+#define SDL_JOYSTICK_WGI 1
+/* #undef SDL_JOYSTICK_DINPUT */
+/* #undef SDL_JOYSTICK_XINPUT */
+/* #undef SDL_JOYSTICK_DUMMY */
+/* #undef SDL_JOYSTICK_IOKIT */
+/* #undef SDL_JOYSTICK_MFI */
+/* #undef SDL_JOYSTICK_LINUX */
+/* #undef SDL_JOYSTICK_OS2 */
+/* #undef SDL_JOYSTICK_USBHID */
+/* #undef SDL_HAVE_MACHINE_JOYSTICK_H */
+#define SDL_JOYSTICK_HIDAPI 1
+/* #undef SDL_JOYSTICK_RAWINPUT */
+/* #undef SDL_JOYSTICK_EMSCRIPTEN */
+#define SDL_JOYSTICK_VIRTUAL 1
+/* #undef SDL_JOYSTICK_VITA */
+/* #undef SDL_JOYSTICK_PSP */
+/* #undef SDL_JOYSTICK_PS2 */
+/* #undef SDL_JOYSTICK_N3DS */
+#define SDL_HAPTIC_DUMMY 1
+/* #undef SDL_HAPTIC_LINUX */
+/* #undef SDL_HAPTIC_IOKIT */
+/* #undef SDL_HAPTIC_DINPUT */
+/* #undef SDL_HAPTIC_XINPUT */
+/* #undef SDL_HAPTIC_ANDROID */
+/* #undef SDL_LIBUSB_DYNAMIC */
+/* #undef SDL_UDEV_DYNAMIC */
+
+/* Enable various sensor drivers */
+/* #undef SDL_SENSOR_ANDROID */
+/* #undef SDL_SENSOR_COREMOTION */
+/* #undef SDL_SENSOR_WINDOWS */
+#define SDL_SENSOR_DUMMY 1
+/* #undef SDL_SENSOR_VITA */
+/* #undef SDL_SENSOR_N3DS */
+
+/* Enable various shared object loading systems */
+/* #undef SDL_LOADSO_DLOPEN */
+/* #undef SDL_LOADSO_DUMMY */
+/* #undef SDL_LOADSO_LDG */
+#define SDL_LOADSO_WINDOWS 1
+/* #undef SDL_LOADSO_OS2 */
+
+/* Enable various threading systems */
+#define SDL_THREAD_GENERIC_COND_SUFFIX 1
+/* #undef SDL_THREAD_PTHREAD */
+/* #undef SDL_THREAD_PTHREAD_RECURSIVE_MUTEX */
+/* #undef SDL_THREAD_PTHREAD_RECURSIVE_MUTEX_NP */
+#define SDL_THREAD_WINDOWS 1
+/* #undef SDL_THREAD_OS2 */
+/* #undef SDL_THREAD_VITA */
+/* #undef SDL_THREAD_PSP */
+/* #undef SDL_THREAD_PS2 */
+/* #undef SDL_THREAD_N3DS */
+
+/* Enable various timer systems */
+/* #undef SDL_TIMER_HAIKU */
+/* #undef SDL_TIMER_DUMMY */
+/* #undef SDL_TIMER_UNIX */
+#define SDL_TIMER_WINDOWS 1
+/* #undef SDL_TIMER_OS2 */
+/* #undef SDL_TIMER_VITA */
+/* #undef SDL_TIMER_PSP */
+/* #undef SDL_TIMER_PS2 */
+/* #undef SDL_TIMER_N3DS */
+
+/* Enable various video drivers */
+/* #undef SDL_VIDEO_DRIVER_ANDROID */
+/* #undef SDL_VIDEO_DRIVER_EMSCRIPTEN */
+/* #undef SDL_VIDEO_DRIVER_HAIKU */
+/* #undef SDL_VIDEO_DRIVER_COCOA */
+/* #undef SDL_VIDEO_DRIVER_UIKIT */
+/* #undef SDL_VIDEO_DRIVER_DIRECTFB */
+/* #undef SDL_VIDEO_DRIVER_DIRECTFB_DYNAMIC */
+#define SDL_VIDEO_DRIVER_DUMMY 1
+#define SDL_VIDEO_DRIVER_OFFSCREEN 1
+/* #undef SDL_VIDEO_DRIVER_WINDOWS */
+#define SDL_VIDEO_DRIVER_WINRT 1
+/* #undef SDL_VIDEO_DRIVER_WAYLAND */
+/* #undef SDL_VIDEO_DRIVER_RPI */
+/* #undef SDL_VIDEO_DRIVER_VIVANTE */
+/* #undef SDL_VIDEO_DRIVER_VIVANTE_VDK */
+/* #undef SDL_VIDEO_DRIVER_OS2 */
+/* #undef SDL_VIDEO_DRIVER_QNX */
+/* #undef SDL_VIDEO_DRIVER_RISCOS */
+/* #undef SDL_VIDEO_DRIVER_PSP */
+/* #undef SDL_VIDEO_DRIVER_PS2 */
+
+/* #undef SDL_VIDEO_DRIVER_KMSDRM */
+/* #undef SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC */
+/* #undef SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC_GBM */
+
+/* #undef SDL_VIDEO_DRIVER_WAYLAND_QT_TOUCH */
+/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC */
+/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_EGL */
+/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_CURSOR */
+/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_XKBCOMMON */
+/* #undef SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR */
+
+/* #undef SDL_VIDEO_DRIVER_X11 */
+/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC */
+/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XEXT */
+/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XCURSOR */
+/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2 */
+/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XFIXES */
+/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR */
+/* #undef SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS */
+/* #undef SDL_VIDEO_DRIVER_X11_XCURSOR */
+/* #undef SDL_VIDEO_DRIVER_X11_XDBE */
+/* #undef SDL_VIDEO_DRIVER_X11_XINPUT2 */
+/* #undef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */
+/* #undef SDL_VIDEO_DRIVER_X11_XFIXES */
+/* #undef SDL_VIDEO_DRIVER_X11_XRANDR */
+/* #undef SDL_VIDEO_DRIVER_X11_XSCRNSAVER */
+/* #undef SDL_VIDEO_DRIVER_X11_XSHAPE */
+/* #undef SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS */
+/* #undef SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM */
+/* #undef SDL_VIDEO_DRIVER_VITA */
+/* #undef SDL_VIDEO_DRIVER_N3DS */
+
+/* #undef SDL_VIDEO_RENDER_D3D */
+#define SDL_VIDEO_RENDER_D3D11 1
+/* #undef SDL_VIDEO_RENDER_D3D12 */
+/* #undef SDL_VIDEO_RENDER_OGL */
+/* #undef SDL_VIDEO_RENDER_OGL_ES */
+#define SDL_VIDEO_RENDER_OGL_ES2 1
+/* #undef SDL_VIDEO_RENDER_DIRECTFB */
+/* #undef SDL_VIDEO_RENDER_METAL */
+/* #undef SDL_VIDEO_RENDER_VITA_GXM */
+/* #undef SDL_VIDEO_RENDER_PS2 */
+/* #undef SDL_VIDEO_RENDER_PSP */
+
+/* Enable OpenGL support */
+/* #undef SDL_VIDEO_OPENGL */
+/* #undef SDL_VIDEO_OPENGL_ES */
+#define SDL_VIDEO_OPENGL_ES2 1
+/* #undef SDL_VIDEO_OPENGL_BGL */
+/* #undef SDL_VIDEO_OPENGL_CGL */
+/* #undef SDL_VIDEO_OPENGL_GLX */
+/* #undef SDL_VIDEO_OPENGL_WGL */
+#define SDL_VIDEO_OPENGL_EGL 1
+/* #undef SDL_VIDEO_OPENGL_OSMESA */
+/* #undef SDL_VIDEO_OPENGL_OSMESA_DYNAMIC */
+
+/* Enable Vulkan support */
+/* #undef SDL_VIDEO_VULKAN */
+
+/* Enable Metal support */
+/* #undef SDL_VIDEO_METAL */
+
+/* Enable system power support */
+/* #undef SDL_POWER_ANDROID */
+/* #undef SDL_POWER_LINUX */
+/* #undef SDL_POWER_WINDOWS */
+#define SDL_POWER_WINRT 1
+/* #undef SDL_POWER_MACOSX */
+/* #undef SDL_POWER_UIKIT */
+/* #undef SDL_POWER_HAIKU */
+/* #undef SDL_POWER_EMSCRIPTEN */
+/* #undef SDL_POWER_HARDWIRED */
+/* #undef SDL_POWER_VITA */
+/* #undef SDL_POWER_PSP */
+/* #undef SDL_POWER_N3DS */
+
+/* Enable system filesystem support */
+/* #undef SDL_FILESYSTEM_ANDROID */
+/* #undef SDL_FILESYSTEM_HAIKU */
+/* #undef SDL_FILESYSTEM_COCOA */
+/* #undef SDL_FILESYSTEM_DUMMY */
+/* #undef SDL_FILESYSTEM_RISCOS */
+/* #undef SDL_FILESYSTEM_UNIX */
+#define SDL_FILESYSTEM_WINDOWS 1
+/* #undef SDL_FILESYSTEM_EMSCRIPTEN */
+/* #undef SDL_FILESYSTEM_OS2 */
+/* #undef SDL_FILESYSTEM_VITA */
+/* #undef SDL_FILESYSTEM_PSP */
+/* #undef SDL_FILESYSTEM_PS2 */
+/* #undef SDL_FILESYSTEM_N3DS */
+
+/* Enable misc subsystem */
+/* #undef SDL_MISC_DUMMY */
+
+/* Enable locale subsystem */
+/* #undef SDL_LOCALE_DUMMY */
+
+/* Enable assembly routines */
+/* #undef SDL_ALTIVEC_BLITTERS */
+/* #undef SDL_ARM_SIMD_BLITTERS */
+/* #undef SDL_ARM_NEON_BLITTERS */
+
+/* Whether SDL_DYNAMIC_API needs dlopen */
+/* #undef DYNAPI_NEEDS_DLOPEN */
+
+/* Enable dynamic libsamplerate support */
+/* #undef SDL_LIBSAMPLERATE_DYNAMIC */
+
+/* Enable ime support */
+/* #undef SDL_USE_IME */
+
+/* Platform specific definitions */
+/* #undef SDL_IPHONE_KEYBOARD */
+/* #undef SDL_IPHONE_LAUNCHSCREEN */
+
+/* #undef SDL_VIDEO_VITA_PIB */
+/* #undef SDL_VIDEO_VITA_PVR */
+/* #undef SDL_VIDEO_VITA_PVR_OGL */
+
+/* #undef SDL_HAVE_LIBDECOR_GET_MIN_MAX */
+
+#if !defined(HAVE_STDINT_H) && !defined(_STDINT_H_)
+/* Most everything except Visual Studio 2008 and earlier has stdint.h now */
+#if defined(_MSC_VER) && (_MSC_VER < 1600)
+typedef signed __int8 int8_t;
+typedef unsigned __int8 uint8_t;
+typedef signed __int16 int16_t;
+typedef unsigned __int16 uint16_t;
+typedef signed __int32 int32_t;
+typedef unsigned __int32 uint32_t;
+typedef signed __int64 int64_t;
+typedef unsigned __int64 uint64_t;
+#ifndef _UINTPTR_T_DEFINED
+#ifdef _WIN64
+typedef unsigned __int64 uintptr_t;
+#else
+typedef unsigned int uintptr_t;
+#endif
+#define _UINTPTR_T_DEFINED
+#endif
+#endif /* Visual Studio 2008 */
+#endif /* !_STDINT_H_ && !HAVE_STDINT_H */
+
+#endif /* SDL_config_h_ */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_copying.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_copying.h
new file mode 100644
index 00000000..bde74318
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_copying.h
@@ -0,0 +1,20 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_cpuinfo.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_cpuinfo.h
new file mode 100644
index 00000000..696a03bf
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_cpuinfo.h
@@ -0,0 +1,603 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* WIKI CATEGORY: CPUInfo */
+
+/**
+ * # CategoryCPUInfo
+ *
+ * CPU feature detection for SDL.
+ *
+ * These functions are largely concerned with reporting if the system has
+ * access to various SIMD instruction sets, but also has other important info
+ * to share, such as number of logical CPU cores.
+ */
+
+#ifndef SDL_cpuinfo_h_
+#define SDL_cpuinfo_h_
+
+#include "SDL_stdinc.h"
+
+/* Need to do this here because intrin.h has C++ code in it */
+/* Visual Studio 2005 has a bug where intrin.h conflicts with winnt.h */
+#if defined(_MSC_VER) && (_MSC_VER >= 1500) && (defined(_M_IX86) || defined(_M_X64))
+#ifdef __clang__
+/* As of Clang 11, '_m_prefetchw' is conflicting with the winnt.h's version,
+ so we define the needed '_m_prefetch' here as a pseudo-header, until the issue is fixed. */
+
+#ifndef __PRFCHWINTRIN_H
+#define __PRFCHWINTRIN_H
+
+static __inline__ void __attribute__((__always_inline__, __nodebug__))
+_m_prefetch(void *__P)
+{
+ __builtin_prefetch (__P, 0, 3 /* _MM_HINT_T0 */);
+}
+
+#endif /* __PRFCHWINTRIN_H */
+#endif /* __clang__ */
+#include
+#ifndef _WIN64
+#ifndef __MMX__
+#define __MMX__
+#endif
+/*
+#ifndef __3dNOW__
+#define __3dNOW__
+#endif
+*/
+#endif
+#ifndef __SSE__
+#define __SSE__
+#endif
+#ifndef __SSE2__
+#define __SSE2__
+#endif
+#ifndef __SSE3__
+#define __SSE3__
+#endif
+#elif defined(__MINGW64_VERSION_MAJOR)
+#include
+#if !defined(SDL_DISABLE_ARM_NEON_H) && defined(__ARM_NEON)
+# include
+#endif
+#else
+/* altivec.h redefining bool causes a number of problems, see bugs 3993 and 4392, so you need to explicitly define SDL_ENABLE_ALTIVEC_H to have it included. */
+#if defined(HAVE_ALTIVEC_H) && defined(__ALTIVEC__) && !defined(__APPLE_ALTIVEC__) && defined(SDL_ENABLE_ALTIVEC_H)
+#include
+#endif
+#if !defined(SDL_DISABLE_ARM_NEON_H)
+# if defined(__ARM_NEON)
+# include
+# elif defined(__WINDOWS__) || defined(__WINRT__) || defined(__GDK__)
+/* Visual Studio doesn't define __ARM_ARCH, but _M_ARM (if set, always 7), and _M_ARM64 (if set, always 1). */
+# if defined(_M_ARM)
+# include
+# include
+# define __ARM_NEON 1 /* Set __ARM_NEON so that it can be used elsewhere, at compile time */
+# endif
+# if defined (_M_ARM64)
+# include
+# include
+# define __ARM_NEON 1 /* Set __ARM_NEON so that it can be used elsewhere, at compile time */
+# define __ARM_ARCH 8
+# endif
+# endif
+#endif
+#endif /* compiler version */
+
+#if defined(__3dNOW__) && !defined(SDL_DISABLE_MM3DNOW_H)
+#include
+#endif
+#if defined(__loongarch_sx) && !defined(SDL_DISABLE_LSX_H)
+#include
+#define __LSX__
+#endif
+#if defined(__loongarch_asx) && !defined(SDL_DISABLE_LASX_H)
+#include
+#define __LASX__
+#endif
+#if defined(HAVE_IMMINTRIN_H) && !defined(SDL_DISABLE_IMMINTRIN_H) && \
+ (defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86))
+#include
+#else
+#if defined(__MMX__) && !defined(SDL_DISABLE_MMINTRIN_H)
+#include
+#endif
+#if defined(__SSE__) && !defined(SDL_DISABLE_XMMINTRIN_H)
+#include
+#endif
+#if defined(__SSE2__) && !defined(SDL_DISABLE_EMMINTRIN_H)
+#include
+#endif
+#if defined(__SSE3__) && !defined(SDL_DISABLE_PMMINTRIN_H)
+#include
+#endif
+#endif /* HAVE_IMMINTRIN_H */
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* This is a guess for the cacheline size used for padding.
+ * Most x86 processors have a 64 byte cache line.
+ * The 64-bit PowerPC processors have a 128 byte cache line.
+ * We'll use the larger value to be generally safe.
+ */
+#define SDL_CACHELINE_SIZE 128
+
+/**
+ * Get the number of CPU cores available.
+ *
+ * \returns the total number of logical CPU cores. On CPUs that include
+ * technologies such as hyperthreading, the number of logical cores
+ * may be more than the number of physical cores.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC int SDLCALL SDL_GetCPUCount(void);
+
+/**
+ * Determine the L1 cache line size of the CPU.
+ *
+ * This is useful for determining multi-threaded structure padding or SIMD
+ * prefetch sizes.
+ *
+ * \returns the L1 cache line size of the CPU, in bytes.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC int SDLCALL SDL_GetCPUCacheLineSize(void);
+
+/**
+ * Determine whether the CPU has the RDTSC instruction.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has the RDTSC instruction or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasRDTSC(void);
+
+/**
+ * Determine whether the CPU has AltiVec features.
+ *
+ * This always returns false on CPUs that aren't using PowerPC instruction
+ * sets.
+ *
+ * \returns SDL_TRUE if the CPU has AltiVec features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasAltiVec(void);
+
+/**
+ * Determine whether the CPU has MMX features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has MMX features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasMMX(void);
+
+/**
+ * Determine whether the CPU has 3DNow! features.
+ *
+ * This always returns false on CPUs that aren't using AMD instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has 3DNow! features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_Has3DNow(void);
+
+/**
+ * Determine whether the CPU has SSE features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has SSE features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasSSE(void);
+
+/**
+ * Determine whether the CPU has SSE2 features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has SSE2 features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasSSE2(void);
+
+/**
+ * Determine whether the CPU has SSE3 features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has SSE3 features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasSSE3(void);
+
+/**
+ * Determine whether the CPU has SSE4.1 features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has SSE4.1 features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasSSE41(void);
+
+/**
+ * Determine whether the CPU has SSE4.2 features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has SSE4.2 features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasSSE42(void);
+
+/**
+ * Determine whether the CPU has AVX features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has AVX features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX2
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasAVX(void);
+
+/**
+ * Determine whether the CPU has AVX2 features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has AVX2 features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.4.
+ *
+ * \sa SDL_Has3DNow
+ * \sa SDL_HasAltiVec
+ * \sa SDL_HasAVX
+ * \sa SDL_HasMMX
+ * \sa SDL_HasRDTSC
+ * \sa SDL_HasSSE
+ * \sa SDL_HasSSE2
+ * \sa SDL_HasSSE3
+ * \sa SDL_HasSSE41
+ * \sa SDL_HasSSE42
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasAVX2(void);
+
+/**
+ * Determine whether the CPU has AVX-512F (foundation) features.
+ *
+ * This always returns false on CPUs that aren't using Intel instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has AVX-512F features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.9.
+ *
+ * \sa SDL_HasAVX
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasAVX512F(void);
+
+/**
+ * Determine whether the CPU has ARM SIMD (ARMv6) features.
+ *
+ * This is different from ARM NEON, which is a different instruction set.
+ *
+ * This always returns false on CPUs that aren't using ARM instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has ARM SIMD features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.12.
+ *
+ * \sa SDL_HasNEON
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasARMSIMD(void);
+
+/**
+ * Determine whether the CPU has NEON (ARM SIMD) features.
+ *
+ * This always returns false on CPUs that aren't using ARM instruction sets.
+ *
+ * \returns SDL_TRUE if the CPU has ARM NEON features or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasNEON(void);
+
+/**
+ * Determine whether the CPU has LSX (LOONGARCH SIMD) features.
+ *
+ * This always returns false on CPUs that aren't using LOONGARCH instruction
+ * sets.
+ *
+ * \returns SDL_TRUE if the CPU has LOONGARCH LSX features or SDL_FALSE if
+ * not.
+ *
+ * \since This function is available since SDL 2.24.0.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasLSX(void);
+
+/**
+ * Determine whether the CPU has LASX (LOONGARCH SIMD) features.
+ *
+ * This always returns false on CPUs that aren't using LOONGARCH instruction
+ * sets.
+ *
+ * \returns SDL_TRUE if the CPU has LOONGARCH LASX features or SDL_FALSE if
+ * not.
+ *
+ * \since This function is available since SDL 2.24.0.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasLASX(void);
+
+/**
+ * Get the amount of RAM configured in the system.
+ *
+ * \returns the amount of RAM configured in the system in MiB.
+ *
+ * \since This function is available since SDL 2.0.1.
+ */
+extern DECLSPEC int SDLCALL SDL_GetSystemRAM(void);
+
+/**
+ * Report the alignment this system needs for SIMD allocations.
+ *
+ * This will return the minimum number of bytes to which a pointer must be
+ * aligned to be compatible with SIMD instructions on the current machine. For
+ * example, if the machine supports SSE only, it will return 16, but if it
+ * supports AVX-512F, it'll return 64 (etc). This only reports values for
+ * instruction sets SDL knows about, so if your SDL build doesn't have
+ * SDL_HasAVX512F(), then it might return 16 for the SSE support it sees and
+ * not 64 for the AVX-512 instructions that exist but SDL doesn't know about.
+ * Plan accordingly.
+ *
+ * \returns the alignment in bytes needed for available, known SIMD
+ * instructions.
+ *
+ * \since This function is available since SDL 2.0.10.
+ */
+extern DECLSPEC size_t SDLCALL SDL_SIMDGetAlignment(void);
+
+/**
+ * Allocate memory in a SIMD-friendly way.
+ *
+ * This will allocate a block of memory that is suitable for use with SIMD
+ * instructions. Specifically, it will be properly aligned and padded for the
+ * system's supported vector instructions.
+ *
+ * The memory returned will be padded such that it is safe to read or write an
+ * incomplete vector at the end of the memory block. This can be useful so you
+ * don't have to drop back to a scalar fallback at the end of your SIMD
+ * processing loop to deal with the final elements without overflowing the
+ * allocated buffer.
+ *
+ * You must free this memory with SDL_FreeSIMD(), not free() or SDL_free() or
+ * delete[], etc.
+ *
+ * Note that SDL will only deal with SIMD instruction sets it is aware of; for
+ * example, SDL 2.0.8 knows that SSE wants 16-byte vectors (SDL_HasSSE()), and
+ * AVX2 wants 32 bytes (SDL_HasAVX2()), but doesn't know that AVX-512 wants
+ * 64. To be clear: if you can't decide to use an instruction set with an
+ * SDL_Has*() function, don't use that instruction set with memory allocated
+ * through here.
+ *
+ * SDL_AllocSIMD(0) will return a non-NULL pointer, assuming the system isn't
+ * out of memory, but you are not allowed to dereference it (because you only
+ * own zero bytes of that buffer).
+ *
+ * \param len The length, in bytes, of the block to allocate. The actual
+ * allocated block might be larger due to padding, etc.
+ * \returns a pointer to the newly-allocated block, NULL if out of memory.
+ *
+ * \since This function is available since SDL 2.0.10.
+ *
+ * \sa SDL_SIMDGetAlignment
+ * \sa SDL_SIMDRealloc
+ * \sa SDL_SIMDFree
+ */
+extern DECLSPEC void * SDLCALL SDL_SIMDAlloc(const size_t len);
+
+/**
+ * Reallocate memory obtained from SDL_SIMDAlloc
+ *
+ * It is not valid to use this function on a pointer from anything but
+ * SDL_SIMDAlloc(). It can't be used on pointers from malloc, realloc,
+ * SDL_malloc, memalign, new[], etc.
+ *
+ * \param mem The pointer obtained from SDL_SIMDAlloc. This function also
+ * accepts NULL, at which point this function is the same as
+ * calling SDL_SIMDAlloc with a NULL pointer.
+ * \param len The length, in bytes, of the block to allocated. The actual
+ * allocated block might be larger due to padding, etc. Passing 0
+ * will return a non-NULL pointer, assuming the system isn't out of
+ * memory.
+ * \returns a pointer to the newly-reallocated block, NULL if out of memory.
+ *
+ * \since This function is available since SDL 2.0.14.
+ *
+ * \sa SDL_SIMDGetAlignment
+ * \sa SDL_SIMDAlloc
+ * \sa SDL_SIMDFree
+ */
+extern DECLSPEC void * SDLCALL SDL_SIMDRealloc(void *mem, const size_t len);
+
+/**
+ * Deallocate memory obtained from SDL_SIMDAlloc
+ *
+ * It is not valid to use this function on a pointer from anything but
+ * SDL_SIMDAlloc() or SDL_SIMDRealloc(). It can't be used on pointers from
+ * malloc, realloc, SDL_malloc, memalign, new[], etc.
+ *
+ * However, SDL_SIMDFree(NULL) is a legal no-op.
+ *
+ * The memory pointed to by `ptr` is no longer valid for access upon return,
+ * and may be returned to the system or reused by a future allocation. The
+ * pointer passed to this function is no longer safe to dereference once this
+ * function returns, and should be discarded.
+ *
+ * \param ptr The pointer, returned from SDL_SIMDAlloc or SDL_SIMDRealloc, to
+ * deallocate. NULL is a legal no-op.
+ *
+ * \since This function is available since SDL 2.0.10.
+ *
+ * \sa SDL_SIMDAlloc
+ * \sa SDL_SIMDRealloc
+ */
+extern DECLSPEC void SDLCALL SDL_SIMDFree(void *ptr);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_cpuinfo_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_egl.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_egl.h
new file mode 100644
index 00000000..31290ec2
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_egl.h
@@ -0,0 +1,2351 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/*
+ * This is a simple file to encapsulate the EGL API headers.
+ */
+
+#if !defined(_MSC_VER) && !defined(__ANDROID__) && !defined(SDL_USE_BUILTIN_OPENGL_DEFINITIONS)
+
+#if defined(__vita__) || defined(__psp2__)
+#include
+#endif
+
+#include
+#include
+
+#else /* _MSC_VER */
+
+/* EGL headers for Visual Studio */
+
+#ifndef __khrplatform_h_
+#define __khrplatform_h_
+
+/*
+** Copyright (c) 2008-2018 The Khronos Group Inc.
+**
+** Permission is hereby granted, free of charge, to any person obtaining a
+** copy of this software and/or associated documentation files (the
+** "Materials"), to deal in the Materials without restriction, including
+** without limitation the rights to use, copy, modify, merge, publish,
+** distribute, sublicense, and/or sell copies of the Materials, and to
+** permit persons to whom the Materials are furnished to do so, subject to
+** the following conditions:
+**
+** The above copyright notice and this permission notice shall be included
+** in all copies or substantial portions of the Materials.
+**
+** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
+*/
+
+/* Khronos platform-specific types and definitions.
+ *
+ * The master copy of khrplatform.h is maintained in the Khronos EGL
+ * Registry repository at https://github.com/KhronosGroup/EGL-Registry
+ * The last semantic modification to khrplatform.h was at commit ID:
+ * 67a3e0864c2d75ea5287b9f3d2eb74a745936692
+ *
+ * Adopters may modify this file to suit their platform. Adopters are
+ * encouraged to submit platform specific modifications to the Khronos
+ * group so that they can be included in future versions of this file.
+ * Please submit changes by filing pull requests or issues on
+ * the EGL Registry repository linked above.
+ *
+ *
+ * See the Implementer's Guidelines for information about where this file
+ * should be located on your system and for more details of its use:
+ * http://www.khronos.org/registry/implementers_guide.pdf
+ *
+ * This file should be included as
+ * #include
+ * by Khronos client API header files that use its types and defines.
+ *
+ * The types in khrplatform.h should only be used to define API-specific types.
+ *
+ * Types defined in khrplatform.h:
+ * khronos_int8_t signed 8 bit
+ * khronos_uint8_t unsigned 8 bit
+ * khronos_int16_t signed 16 bit
+ * khronos_uint16_t unsigned 16 bit
+ * khronos_int32_t signed 32 bit
+ * khronos_uint32_t unsigned 32 bit
+ * khronos_int64_t signed 64 bit
+ * khronos_uint64_t unsigned 64 bit
+ * khronos_intptr_t signed same number of bits as a pointer
+ * khronos_uintptr_t unsigned same number of bits as a pointer
+ * khronos_ssize_t signed size
+ * khronos_usize_t unsigned size
+ * khronos_float_t signed 32 bit floating point
+ * khronos_time_ns_t unsigned 64 bit time in nanoseconds
+ * khronos_utime_nanoseconds_t unsigned time interval or absolute time in
+ * nanoseconds
+ * khronos_stime_nanoseconds_t signed time interval in nanoseconds
+ * khronos_boolean_enum_t enumerated boolean type. This should
+ * only be used as a base type when a client API's boolean type is
+ * an enum. Client APIs which use an integer or other type for
+ * booleans cannot use this as the base type for their boolean.
+ *
+ * Tokens defined in khrplatform.h:
+ *
+ * KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
+ *
+ * KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
+ * KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
+ *
+ * Calling convention macros defined in this file:
+ * KHRONOS_APICALL
+ * KHRONOS_APIENTRY
+ * KHRONOS_APIATTRIBUTES
+ *
+ * These may be used in function prototypes as:
+ *
+ * KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
+ * int arg1,
+ * int arg2) KHRONOS_APIATTRIBUTES;
+ */
+
+#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
+# define KHRONOS_STATIC 1
+#endif
+
+/*-------------------------------------------------------------------------
+ * Definition of KHRONOS_APICALL
+ *-------------------------------------------------------------------------
+ * This precedes the return type of the function in the function prototype.
+ */
+#if defined(KHRONOS_STATIC)
+ /* If the preprocessor constant KHRONOS_STATIC is defined, make the
+ * header compatible with static linking. */
+# define KHRONOS_APICALL
+#elif defined(_WIN32)
+# define KHRONOS_APICALL __declspec(dllimport)
+#elif defined (__SYMBIAN32__)
+# define KHRONOS_APICALL IMPORT_C
+#elif defined(__ANDROID__)
+# define KHRONOS_APICALL __attribute__((visibility("default")))
+#else
+# define KHRONOS_APICALL
+#endif
+
+/*-------------------------------------------------------------------------
+ * Definition of KHRONOS_APIENTRY
+ *-------------------------------------------------------------------------
+ * This follows the return type of the function and precedes the function
+ * name in the function prototype.
+ */
+#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
+ /* Win32 but not WinCE */
+# define KHRONOS_APIENTRY __stdcall
+#else
+# define KHRONOS_APIENTRY
+#endif
+
+/*-------------------------------------------------------------------------
+ * Definition of KHRONOS_APIATTRIBUTES
+ *-------------------------------------------------------------------------
+ * This follows the closing parenthesis of the function prototype arguments.
+ */
+#if defined (__ARMCC_2__)
+#define KHRONOS_APIATTRIBUTES __softfp
+#else
+#define KHRONOS_APIATTRIBUTES
+#endif
+
+/*-------------------------------------------------------------------------
+ * basic type definitions
+ *-----------------------------------------------------------------------*/
+#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
+
+
+/*
+ * Using
+ */
+#include
+typedef int32_t khronos_int32_t;
+typedef uint32_t khronos_uint32_t;
+typedef int64_t khronos_int64_t;
+typedef uint64_t khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+/*
+ * To support platform where unsigned long cannot be used interchangeably with
+ * inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
+ * Ideally, we could just use (u)intptr_t everywhere, but this could result in
+ * ABI breakage if khronos_uintptr_t is changed from unsigned long to
+ * unsigned long long or similar (this results in different C++ name mangling).
+ * To avoid changes for existing platforms, we restrict usage of intptr_t to
+ * platforms where the size of a pointer is larger than the size of long.
+ */
+#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
+#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
+#define KHRONOS_USE_INTPTR_T
+#endif
+#endif
+
+#elif defined(__VMS ) || defined(__sgi)
+
+/*
+ * Using
+ */
+#include
+typedef int32_t khronos_int32_t;
+typedef uint32_t khronos_uint32_t;
+typedef int64_t khronos_int64_t;
+typedef uint64_t khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
+
+/*
+ * Win32
+ */
+typedef __int32 khronos_int32_t;
+typedef unsigned __int32 khronos_uint32_t;
+typedef __int64 khronos_int64_t;
+typedef unsigned __int64 khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#elif defined(__sun__) || defined(__digital__)
+
+/*
+ * Sun or Digital
+ */
+typedef int khronos_int32_t;
+typedef unsigned int khronos_uint32_t;
+#if defined(__arch64__) || defined(_LP64)
+typedef long int khronos_int64_t;
+typedef unsigned long int khronos_uint64_t;
+#else
+typedef long long int khronos_int64_t;
+typedef unsigned long long int khronos_uint64_t;
+#endif /* __arch64__ */
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#elif 0
+
+/*
+ * Hypothetical platform with no float or int64 support
+ */
+typedef int khronos_int32_t;
+typedef unsigned int khronos_uint32_t;
+#define KHRONOS_SUPPORT_INT64 0
+#define KHRONOS_SUPPORT_FLOAT 0
+
+#else
+
+/*
+ * Generic fallback
+ */
+#include
+typedef int32_t khronos_int32_t;
+typedef uint32_t khronos_uint32_t;
+typedef int64_t khronos_int64_t;
+typedef uint64_t khronos_uint64_t;
+#define KHRONOS_SUPPORT_INT64 1
+#define KHRONOS_SUPPORT_FLOAT 1
+
+#endif
+
+
+/*
+ * Types that are (so far) the same on all platforms
+ */
+typedef signed char khronos_int8_t;
+typedef unsigned char khronos_uint8_t;
+typedef signed short int khronos_int16_t;
+typedef unsigned short int khronos_uint16_t;
+
+/*
+ * Types that differ between LLP64 and LP64 architectures - in LLP64,
+ * pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
+ * to be the only LLP64 architecture in current use.
+ */
+#ifdef KHRONOS_USE_INTPTR_T
+typedef intptr_t khronos_intptr_t;
+typedef uintptr_t khronos_uintptr_t;
+#elif defined(_WIN64)
+typedef signed long long int khronos_intptr_t;
+typedef unsigned long long int khronos_uintptr_t;
+#else
+typedef signed long int khronos_intptr_t;
+typedef unsigned long int khronos_uintptr_t;
+#endif
+
+#if defined(_WIN64)
+typedef signed long long int khronos_ssize_t;
+typedef unsigned long long int khronos_usize_t;
+#else
+typedef signed long int khronos_ssize_t;
+typedef unsigned long int khronos_usize_t;
+#endif
+
+#if KHRONOS_SUPPORT_FLOAT
+/*
+ * Float type
+ */
+typedef float khronos_float_t;
+#endif
+
+#if KHRONOS_SUPPORT_INT64
+/* Time types
+ *
+ * These types can be used to represent a time interval in nanoseconds or
+ * an absolute Unadjusted System Time. Unadjusted System Time is the number
+ * of nanoseconds since some arbitrary system event (e.g. since the last
+ * time the system booted). The Unadjusted System Time is an unsigned
+ * 64 bit value that wraps back to 0 every 584 years. Time intervals
+ * may be either signed or unsigned.
+ */
+typedef khronos_uint64_t khronos_utime_nanoseconds_t;
+typedef khronos_int64_t khronos_stime_nanoseconds_t;
+#endif
+
+/*
+ * Dummy value used to pad enum types to 32 bits.
+ */
+#ifndef KHRONOS_MAX_ENUM
+#define KHRONOS_MAX_ENUM 0x7FFFFFFF
+#endif
+
+/*
+ * Enumerated boolean type
+ *
+ * Values other than zero should be considered to be true. Therefore
+ * comparisons should not be made against KHRONOS_TRUE.
+ */
+typedef enum {
+ KHRONOS_FALSE = 0,
+ KHRONOS_TRUE = 1,
+ KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
+} khronos_boolean_enum_t;
+
+#endif /* __khrplatform_h_ */
+
+
+#ifndef __eglplatform_h_
+#define __eglplatform_h_
+
+/*
+** Copyright 2007-2020 The Khronos Group Inc.
+** SPDX-License-Identifier: Apache-2.0
+*/
+
+/* Platform-specific types and definitions for egl.h
+ *
+ * Adopters may modify khrplatform.h and this file to suit their platform.
+ * You are encouraged to submit all modifications to the Khronos group so that
+ * they can be included in future versions of this file. Please submit changes
+ * by filing an issue or pull request on the public Khronos EGL Registry, at
+ * https://www.github.com/KhronosGroup/EGL-Registry/
+ */
+
+/*#include */
+
+/* Macros used in EGL function prototype declarations.
+ *
+ * EGL functions should be prototyped as:
+ *
+ * EGLAPI return-type EGLAPIENTRY eglFunction(arguments);
+ * typedef return-type (EXPAPIENTRYP PFNEGLFUNCTIONPROC) (arguments);
+ *
+ * KHRONOS_APICALL and KHRONOS_APIENTRY are defined in KHR/khrplatform.h
+ */
+
+#ifndef EGLAPI
+#define EGLAPI KHRONOS_APICALL
+#endif
+
+#ifndef EGLAPIENTRY
+#define EGLAPIENTRY KHRONOS_APIENTRY
+#endif
+#define EGLAPIENTRYP EGLAPIENTRY*
+
+/* The types NativeDisplayType, NativeWindowType, and NativePixmapType
+ * are aliases of window-system-dependent types, such as X Display * or
+ * Windows Device Context. They must be defined in platform-specific
+ * code below. The EGL-prefixed versions of Native*Type are the same
+ * types, renamed in EGL 1.3 so all types in the API start with "EGL".
+ *
+ * Khronos STRONGLY RECOMMENDS that you use the default definitions
+ * provided below, since these changes affect both binary and source
+ * portability of applications using EGL running on different EGL
+ * implementations.
+ */
+
+#if defined(EGL_NO_PLATFORM_SPECIFIC_TYPES)
+
+typedef void *EGLNativeDisplayType;
+typedef void *EGLNativePixmapType;
+typedef void *EGLNativeWindowType;
+
+#elif defined(_WIN32) || defined(__VC32__) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__) /* Win32 and WinCE */
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN 1
+#endif
+#include
+
+typedef HDC EGLNativeDisplayType;
+typedef HBITMAP EGLNativePixmapType;
+typedef HWND EGLNativeWindowType;
+
+#elif defined(__EMSCRIPTEN__)
+
+typedef int EGLNativeDisplayType;
+typedef int EGLNativePixmapType;
+typedef int EGLNativeWindowType;
+
+#elif defined(__WINSCW__) || defined(__SYMBIAN32__) /* Symbian */
+
+typedef int EGLNativeDisplayType;
+typedef void *EGLNativePixmapType;
+typedef void *EGLNativeWindowType;
+
+#elif defined(WL_EGL_PLATFORM)
+
+typedef struct wl_display *EGLNativeDisplayType;
+typedef struct wl_egl_pixmap *EGLNativePixmapType;
+typedef struct wl_egl_window *EGLNativeWindowType;
+
+#elif defined(__GBM__)
+
+typedef struct gbm_device *EGLNativeDisplayType;
+typedef struct gbm_bo *EGLNativePixmapType;
+typedef void *EGLNativeWindowType;
+
+#elif defined(__ANDROID__) || defined(ANDROID)
+
+struct ANativeWindow;
+struct egl_native_pixmap_t;
+
+typedef void* EGLNativeDisplayType;
+typedef struct egl_native_pixmap_t* EGLNativePixmapType;
+typedef struct ANativeWindow* EGLNativeWindowType;
+
+#elif defined(USE_OZONE)
+
+typedef intptr_t EGLNativeDisplayType;
+typedef intptr_t EGLNativePixmapType;
+typedef intptr_t EGLNativeWindowType;
+
+#elif defined(USE_X11)
+
+/* X11 (tentative) */
+#include
+#include
+
+typedef Display *EGLNativeDisplayType;
+typedef Pixmap EGLNativePixmapType;
+typedef Window EGLNativeWindowType;
+
+#elif defined(__unix__)
+
+typedef void *EGLNativeDisplayType;
+typedef khronos_uintptr_t EGLNativePixmapType;
+typedef khronos_uintptr_t EGLNativeWindowType;
+
+#elif defined(__APPLE__)
+
+typedef int EGLNativeDisplayType;
+typedef void *EGLNativePixmapType;
+typedef void *EGLNativeWindowType;
+
+#elif defined(__HAIKU__)
+
+#include
+
+typedef void *EGLNativeDisplayType;
+typedef khronos_uintptr_t EGLNativePixmapType;
+typedef khronos_uintptr_t EGLNativeWindowType;
+
+#elif defined(__Fuchsia__)
+
+typedef void *EGLNativeDisplayType;
+typedef khronos_uintptr_t EGLNativePixmapType;
+typedef khronos_uintptr_t EGLNativeWindowType;
+
+#else
+#error "Platform not recognized"
+#endif
+
+/* EGL 1.2 types, renamed for consistency in EGL 1.3 */
+typedef EGLNativeDisplayType NativeDisplayType;
+typedef EGLNativePixmapType NativePixmapType;
+typedef EGLNativeWindowType NativeWindowType;
+
+
+/* Define EGLint. This must be a signed integral type large enough to contain
+ * all legal attribute names and values passed into and out of EGL, whether
+ * their type is boolean, bitmask, enumerant (symbolic constant), integer,
+ * handle, or other. While in general a 32-bit integer will suffice, if
+ * handles are 64 bit types, then EGLint should be defined as a signed 64-bit
+ * integer type.
+ */
+typedef khronos_int32_t EGLint;
+
+
+/* C++ / C typecast macros for special EGL handle values */
+#if defined(__cplusplus)
+#define EGL_CAST(type, value) (static_cast(value))
+#else
+#define EGL_CAST(type, value) ((type) (value))
+#endif
+
+#endif /* __eglplatform_h */
+
+
+#ifndef __egl_h_
+#define __egl_h_ 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+** Copyright 2013-2020 The Khronos Group Inc.
+** SPDX-License-Identifier: Apache-2.0
+**
+** This header is generated from the Khronos EGL XML API Registry.
+** The current version of the Registry, generator scripts
+** used to make the header, and the header can be found at
+** http://www.khronos.org/registry/egl
+**
+** Khronos $Git commit SHA1: 6fb1daea15 $ on $Git commit date: 2022-05-25 09:41:13 -0600 $
+*/
+
+/*#include */
+
+#ifndef EGL_EGL_PROTOTYPES
+#define EGL_EGL_PROTOTYPES 1
+#endif
+
+/* Generated on date 20220525 */
+
+/* Generated C header for:
+ * API: egl
+ * Versions considered: .*
+ * Versions emitted: .*
+ * Default extensions included: None
+ * Additional extensions included: _nomatch_^
+ * Extensions removed: _nomatch_^
+ */
+
+#ifndef EGL_VERSION_1_0
+#define EGL_VERSION_1_0 1
+typedef unsigned int EGLBoolean;
+typedef void *EGLDisplay;
+/*#include */
+/*#include */
+typedef void *EGLConfig;
+typedef void *EGLSurface;
+typedef void *EGLContext;
+typedef void (*__eglMustCastToProperFunctionPointerType)(void);
+#define EGL_ALPHA_SIZE 0x3021
+#define EGL_BAD_ACCESS 0x3002
+#define EGL_BAD_ALLOC 0x3003
+#define EGL_BAD_ATTRIBUTE 0x3004
+#define EGL_BAD_CONFIG 0x3005
+#define EGL_BAD_CONTEXT 0x3006
+#define EGL_BAD_CURRENT_SURFACE 0x3007
+#define EGL_BAD_DISPLAY 0x3008
+#define EGL_BAD_MATCH 0x3009
+#define EGL_BAD_NATIVE_PIXMAP 0x300A
+#define EGL_BAD_NATIVE_WINDOW 0x300B
+#define EGL_BAD_PARAMETER 0x300C
+#define EGL_BAD_SURFACE 0x300D
+#define EGL_BLUE_SIZE 0x3022
+#define EGL_BUFFER_SIZE 0x3020
+#define EGL_CONFIG_CAVEAT 0x3027
+#define EGL_CONFIG_ID 0x3028
+#define EGL_CORE_NATIVE_ENGINE 0x305B
+#define EGL_DEPTH_SIZE 0x3025
+#define EGL_DONT_CARE EGL_CAST(EGLint,-1)
+#define EGL_DRAW 0x3059
+#define EGL_EXTENSIONS 0x3055
+#define EGL_FALSE 0
+#define EGL_GREEN_SIZE 0x3023
+#define EGL_HEIGHT 0x3056
+#define EGL_LARGEST_PBUFFER 0x3058
+#define EGL_LEVEL 0x3029
+#define EGL_MAX_PBUFFER_HEIGHT 0x302A
+#define EGL_MAX_PBUFFER_PIXELS 0x302B
+#define EGL_MAX_PBUFFER_WIDTH 0x302C
+#define EGL_NATIVE_RENDERABLE 0x302D
+#define EGL_NATIVE_VISUAL_ID 0x302E
+#define EGL_NATIVE_VISUAL_TYPE 0x302F
+#define EGL_NONE 0x3038
+#define EGL_NON_CONFORMANT_CONFIG 0x3051
+#define EGL_NOT_INITIALIZED 0x3001
+#define EGL_NO_CONTEXT EGL_CAST(EGLContext,0)
+#define EGL_NO_DISPLAY EGL_CAST(EGLDisplay,0)
+#define EGL_NO_SURFACE EGL_CAST(EGLSurface,0)
+#define EGL_PBUFFER_BIT 0x0001
+#define EGL_PIXMAP_BIT 0x0002
+#define EGL_READ 0x305A
+#define EGL_RED_SIZE 0x3024
+#define EGL_SAMPLES 0x3031
+#define EGL_SAMPLE_BUFFERS 0x3032
+#define EGL_SLOW_CONFIG 0x3050
+#define EGL_STENCIL_SIZE 0x3026
+#define EGL_SUCCESS 0x3000
+#define EGL_SURFACE_TYPE 0x3033
+#define EGL_TRANSPARENT_BLUE_VALUE 0x3035
+#define EGL_TRANSPARENT_GREEN_VALUE 0x3036
+#define EGL_TRANSPARENT_RED_VALUE 0x3037
+#define EGL_TRANSPARENT_RGB 0x3052
+#define EGL_TRANSPARENT_TYPE 0x3034
+#define EGL_TRUE 1
+#define EGL_VENDOR 0x3053
+#define EGL_VERSION 0x3054
+#define EGL_WIDTH 0x3057
+#define EGL_WINDOW_BIT 0x0004
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCHOOSECONFIGPROC) (EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOPYBUFFERSPROC) (EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target);
+typedef EGLContext (EGLAPIENTRYP PFNEGLCREATECONTEXTPROC) (EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPBUFFERSURFACEPROC) (EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPIXMAPSURFACEPROC) (EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEWINDOWSURFACEPROC) (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYCONTEXTPROC) (EGLDisplay dpy, EGLContext ctx);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYSURFACEPROC) (EGLDisplay dpy, EGLSurface surface);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETCONFIGATTRIBPROC) (EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETCONFIGSPROC) (EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);
+typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETCURRENTDISPLAYPROC) (void);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLGETCURRENTSURFACEPROC) (EGLint readdraw);
+typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETDISPLAYPROC) (EGLNativeDisplayType display_id);
+typedef EGLint (EGLAPIENTRYP PFNEGLGETERRORPROC) (void);
+typedef __eglMustCastToProperFunctionPointerType (EGLAPIENTRYP PFNEGLGETPROCADDRESSPROC) (const char *procname);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLINITIALIZEPROC) (EGLDisplay dpy, EGLint *major, EGLint *minor);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLMAKECURRENTPROC) (EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYCONTEXTPROC) (EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value);
+typedef const char *(EGLAPIENTRYP PFNEGLQUERYSTRINGPROC) (EGLDisplay dpy, EGLint name);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSURFACEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPBUFFERSPROC) (EGLDisplay dpy, EGLSurface surface);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLTERMINATEPROC) (EGLDisplay dpy);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITGLPROC) (void);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITNATIVEPROC) (EGLint engine);
+#if EGL_EGL_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglChooseConfig (EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
+EGLAPI EGLBoolean EGLAPIENTRY eglCopyBuffers (EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target);
+EGLAPI EGLContext EGLAPIENTRY eglCreateContext (EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePbufferSurface (EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list);
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePixmapSurface (EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list);
+EGLAPI EGLSurface EGLAPIENTRY eglCreateWindowSurface (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroyContext (EGLDisplay dpy, EGLContext ctx);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroySurface (EGLDisplay dpy, EGLSurface surface);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetConfigAttrib (EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetConfigs (EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);
+EGLAPI EGLDisplay EGLAPIENTRY eglGetCurrentDisplay (void);
+EGLAPI EGLSurface EGLAPIENTRY eglGetCurrentSurface (EGLint readdraw);
+EGLAPI EGLDisplay EGLAPIENTRY eglGetDisplay (EGLNativeDisplayType display_id);
+EGLAPI EGLint EGLAPIENTRY eglGetError (void);
+EGLAPI __eglMustCastToProperFunctionPointerType EGLAPIENTRY eglGetProcAddress (const char *procname);
+EGLAPI EGLBoolean EGLAPIENTRY eglInitialize (EGLDisplay dpy, EGLint *major, EGLint *minor);
+EGLAPI EGLBoolean EGLAPIENTRY eglMakeCurrent (EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryContext (EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value);
+EGLAPI const char *EGLAPIENTRY eglQueryString (EGLDisplay dpy, EGLint name);
+EGLAPI EGLBoolean EGLAPIENTRY eglQuerySurface (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value);
+EGLAPI EGLBoolean EGLAPIENTRY eglSwapBuffers (EGLDisplay dpy, EGLSurface surface);
+EGLAPI EGLBoolean EGLAPIENTRY eglTerminate (EGLDisplay dpy);
+EGLAPI EGLBoolean EGLAPIENTRY eglWaitGL (void);
+EGLAPI EGLBoolean EGLAPIENTRY eglWaitNative (EGLint engine);
+#endif
+#endif /* EGL_VERSION_1_0 */
+
+#ifndef EGL_VERSION_1_1
+#define EGL_VERSION_1_1 1
+#define EGL_BACK_BUFFER 0x3084
+#define EGL_BIND_TO_TEXTURE_RGB 0x3039
+#define EGL_BIND_TO_TEXTURE_RGBA 0x303A
+#define EGL_CONTEXT_LOST 0x300E
+#define EGL_MIN_SWAP_INTERVAL 0x303B
+#define EGL_MAX_SWAP_INTERVAL 0x303C
+#define EGL_MIPMAP_TEXTURE 0x3082
+#define EGL_MIPMAP_LEVEL 0x3083
+#define EGL_NO_TEXTURE 0x305C
+#define EGL_TEXTURE_2D 0x305F
+#define EGL_TEXTURE_FORMAT 0x3080
+#define EGL_TEXTURE_RGB 0x305D
+#define EGL_TEXTURE_RGBA 0x305E
+#define EGL_TEXTURE_TARGET 0x3081
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLBINDTEXIMAGEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLRELEASETEXIMAGEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSURFACEATTRIBPROC) (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPINTERVALPROC) (EGLDisplay dpy, EGLint interval);
+#if EGL_EGL_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglBindTexImage (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
+EGLAPI EGLBoolean EGLAPIENTRY eglReleaseTexImage (EGLDisplay dpy, EGLSurface surface, EGLint buffer);
+EGLAPI EGLBoolean EGLAPIENTRY eglSurfaceAttrib (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value);
+EGLAPI EGLBoolean EGLAPIENTRY eglSwapInterval (EGLDisplay dpy, EGLint interval);
+#endif
+#endif /* EGL_VERSION_1_1 */
+
+#ifndef EGL_VERSION_1_2
+#define EGL_VERSION_1_2 1
+typedef unsigned int EGLenum;
+typedef void *EGLClientBuffer;
+#define EGL_ALPHA_FORMAT 0x3088
+#define EGL_ALPHA_FORMAT_NONPRE 0x308B
+#define EGL_ALPHA_FORMAT_PRE 0x308C
+#define EGL_ALPHA_MASK_SIZE 0x303E
+#define EGL_BUFFER_PRESERVED 0x3094
+#define EGL_BUFFER_DESTROYED 0x3095
+#define EGL_CLIENT_APIS 0x308D
+#define EGL_COLORSPACE 0x3087
+#define EGL_COLORSPACE_sRGB 0x3089
+#define EGL_COLORSPACE_LINEAR 0x308A
+#define EGL_COLOR_BUFFER_TYPE 0x303F
+#define EGL_CONTEXT_CLIENT_TYPE 0x3097
+#define EGL_DISPLAY_SCALING 10000
+#define EGL_HORIZONTAL_RESOLUTION 0x3090
+#define EGL_LUMINANCE_BUFFER 0x308F
+#define EGL_LUMINANCE_SIZE 0x303D
+#define EGL_OPENGL_ES_BIT 0x0001
+#define EGL_OPENVG_BIT 0x0002
+#define EGL_OPENGL_ES_API 0x30A0
+#define EGL_OPENVG_API 0x30A1
+#define EGL_OPENVG_IMAGE 0x3096
+#define EGL_PIXEL_ASPECT_RATIO 0x3092
+#define EGL_RENDERABLE_TYPE 0x3040
+#define EGL_RENDER_BUFFER 0x3086
+#define EGL_RGB_BUFFER 0x308E
+#define EGL_SINGLE_BUFFER 0x3085
+#define EGL_SWAP_BEHAVIOR 0x3093
+#define EGL_UNKNOWN EGL_CAST(EGLint,-1)
+#define EGL_VERTICAL_RESOLUTION 0x3091
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLBINDAPIPROC) (EGLenum api);
+typedef EGLenum (EGLAPIENTRYP PFNEGLQUERYAPIPROC) (void);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC) (EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLRELEASETHREADPROC) (void);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITCLIENTPROC) (void);
+#if EGL_EGL_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglBindAPI (EGLenum api);
+EGLAPI EGLenum EGLAPIENTRY eglQueryAPI (void);
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePbufferFromClientBuffer (EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglReleaseThread (void);
+EGLAPI EGLBoolean EGLAPIENTRY eglWaitClient (void);
+#endif
+#endif /* EGL_VERSION_1_2 */
+
+#ifndef EGL_VERSION_1_3
+#define EGL_VERSION_1_3 1
+#define EGL_CONFORMANT 0x3042
+#define EGL_CONTEXT_CLIENT_VERSION 0x3098
+#define EGL_MATCH_NATIVE_PIXMAP 0x3041
+#define EGL_OPENGL_ES2_BIT 0x0004
+#define EGL_VG_ALPHA_FORMAT 0x3088
+#define EGL_VG_ALPHA_FORMAT_NONPRE 0x308B
+#define EGL_VG_ALPHA_FORMAT_PRE 0x308C
+#define EGL_VG_ALPHA_FORMAT_PRE_BIT 0x0040
+#define EGL_VG_COLORSPACE 0x3087
+#define EGL_VG_COLORSPACE_sRGB 0x3089
+#define EGL_VG_COLORSPACE_LINEAR 0x308A
+#define EGL_VG_COLORSPACE_LINEAR_BIT 0x0020
+#endif /* EGL_VERSION_1_3 */
+
+#ifndef EGL_VERSION_1_4
+#define EGL_VERSION_1_4 1
+#define EGL_DEFAULT_DISPLAY EGL_CAST(EGLNativeDisplayType,0)
+#define EGL_MULTISAMPLE_RESOLVE_BOX_BIT 0x0200
+#define EGL_MULTISAMPLE_RESOLVE 0x3099
+#define EGL_MULTISAMPLE_RESOLVE_DEFAULT 0x309A
+#define EGL_MULTISAMPLE_RESOLVE_BOX 0x309B
+#define EGL_OPENGL_API 0x30A2
+#define EGL_OPENGL_BIT 0x0008
+#define EGL_SWAP_BEHAVIOR_PRESERVED_BIT 0x0400
+typedef EGLContext (EGLAPIENTRYP PFNEGLGETCURRENTCONTEXTPROC) (void);
+#if EGL_EGL_PROTOTYPES
+EGLAPI EGLContext EGLAPIENTRY eglGetCurrentContext (void);
+#endif
+#endif /* EGL_VERSION_1_4 */
+
+#ifndef EGL_VERSION_1_5
+#define EGL_VERSION_1_5 1
+typedef void *EGLSync;
+typedef intptr_t EGLAttrib;
+typedef khronos_utime_nanoseconds_t EGLTime;
+typedef void *EGLImage;
+#define EGL_CONTEXT_MAJOR_VERSION 0x3098
+#define EGL_CONTEXT_MINOR_VERSION 0x30FB
+#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
+#define EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY 0x31BD
+#define EGL_NO_RESET_NOTIFICATION 0x31BE
+#define EGL_LOSE_CONTEXT_ON_RESET 0x31BF
+#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
+#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT 0x00000002
+#define EGL_CONTEXT_OPENGL_DEBUG 0x31B0
+#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE 0x31B1
+#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS 0x31B2
+#define EGL_OPENGL_ES3_BIT 0x00000040
+#define EGL_CL_EVENT_HANDLE 0x309C
+#define EGL_SYNC_CL_EVENT 0x30FE
+#define EGL_SYNC_CL_EVENT_COMPLETE 0x30FF
+#define EGL_SYNC_PRIOR_COMMANDS_COMPLETE 0x30F0
+#define EGL_SYNC_TYPE 0x30F7
+#define EGL_SYNC_STATUS 0x30F1
+#define EGL_SYNC_CONDITION 0x30F8
+#define EGL_SIGNALED 0x30F2
+#define EGL_UNSIGNALED 0x30F3
+#define EGL_SYNC_FLUSH_COMMANDS_BIT 0x0001
+#define EGL_FOREVER 0xFFFFFFFFFFFFFFFFull
+#define EGL_TIMEOUT_EXPIRED 0x30F5
+#define EGL_CONDITION_SATISFIED 0x30F6
+#define EGL_NO_SYNC EGL_CAST(EGLSync,0)
+#define EGL_SYNC_FENCE 0x30F9
+#define EGL_GL_COLORSPACE 0x309D
+#define EGL_GL_COLORSPACE_SRGB 0x3089
+#define EGL_GL_COLORSPACE_LINEAR 0x308A
+#define EGL_GL_RENDERBUFFER 0x30B9
+#define EGL_GL_TEXTURE_2D 0x30B1
+#define EGL_GL_TEXTURE_LEVEL 0x30BC
+#define EGL_GL_TEXTURE_3D 0x30B2
+#define EGL_GL_TEXTURE_ZOFFSET 0x30BD
+#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x30B3
+#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x30B4
+#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x30B5
+#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x30B6
+#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x30B7
+#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x30B8
+#define EGL_IMAGE_PRESERVED 0x30D2
+#define EGL_NO_IMAGE EGL_CAST(EGLImage,0)
+typedef EGLSync (EGLAPIENTRYP PFNEGLCREATESYNCPROC) (EGLDisplay dpy, EGLenum type, const EGLAttrib *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYSYNCPROC) (EGLDisplay dpy, EGLSync sync);
+typedef EGLint (EGLAPIENTRYP PFNEGLCLIENTWAITSYNCPROC) (EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETSYNCATTRIBPROC) (EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib *value);
+typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEPROC) (EGLDisplay dpy, EGLImage image);
+typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPLATFORMWINDOWSURFACEPROC) (EGLDisplay dpy, EGLConfig config, void *native_window, const EGLAttrib *attrib_list);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC) (EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLAttrib *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLWAITSYNCPROC) (EGLDisplay dpy, EGLSync sync, EGLint flags);
+#if EGL_EGL_PROTOTYPES
+EGLAPI EGLSync EGLAPIENTRY eglCreateSync (EGLDisplay dpy, EGLenum type, const EGLAttrib *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroySync (EGLDisplay dpy, EGLSync sync);
+EGLAPI EGLint EGLAPIENTRY eglClientWaitSync (EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetSyncAttrib (EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib *value);
+EGLAPI EGLImage EGLAPIENTRY eglCreateImage (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroyImage (EGLDisplay dpy, EGLImage image);
+EGLAPI EGLDisplay EGLAPIENTRY eglGetPlatformDisplay (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePlatformWindowSurface (EGLDisplay dpy, EGLConfig config, void *native_window, const EGLAttrib *attrib_list);
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePlatformPixmapSurface (EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLAttrib *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglWaitSync (EGLDisplay dpy, EGLSync sync, EGLint flags);
+#endif
+#endif /* EGL_VERSION_1_5 */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __egl_h_ */
+
+
+#ifndef __eglext_h_
+#define __eglext_h_ 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+** Copyright 2013-2020 The Khronos Group Inc.
+** SPDX-License-Identifier: Apache-2.0
+**
+** This header is generated from the Khronos EGL XML API Registry.
+** The current version of the Registry, generator scripts
+** used to make the header, and the header can be found at
+** http://www.khronos.org/registry/egl
+**
+** Khronos $Git commit SHA1: 6fb1daea15 $ on $Git commit date: 2022-05-25 09:41:13 -0600 $
+*/
+
+/*#include */
+
+#define EGL_EGLEXT_VERSION 20220525
+
+/* Generated C header for:
+ * API: egl
+ * Versions considered: .*
+ * Versions emitted: _nomatch_^
+ * Default extensions included: egl
+ * Additional extensions included: _nomatch_^
+ * Extensions removed: _nomatch_^
+ */
+
+#ifndef EGL_KHR_cl_event
+#define EGL_KHR_cl_event 1
+#define EGL_CL_EVENT_HANDLE_KHR 0x309C
+#define EGL_SYNC_CL_EVENT_KHR 0x30FE
+#define EGL_SYNC_CL_EVENT_COMPLETE_KHR 0x30FF
+#endif /* EGL_KHR_cl_event */
+
+#ifndef EGL_KHR_cl_event2
+#define EGL_KHR_cl_event2 1
+typedef void *EGLSyncKHR;
+typedef intptr_t EGLAttribKHR;
+typedef EGLSyncKHR (EGLAPIENTRYP PFNEGLCREATESYNC64KHRPROC) (EGLDisplay dpy, EGLenum type, const EGLAttribKHR *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLSyncKHR EGLAPIENTRY eglCreateSync64KHR (EGLDisplay dpy, EGLenum type, const EGLAttribKHR *attrib_list);
+#endif
+#endif /* EGL_KHR_cl_event2 */
+
+#ifndef EGL_KHR_client_get_all_proc_addresses
+#define EGL_KHR_client_get_all_proc_addresses 1
+#endif /* EGL_KHR_client_get_all_proc_addresses */
+
+#ifndef EGL_KHR_config_attribs
+#define EGL_KHR_config_attribs 1
+#define EGL_CONFORMANT_KHR 0x3042
+#define EGL_VG_COLORSPACE_LINEAR_BIT_KHR 0x0020
+#define EGL_VG_ALPHA_FORMAT_PRE_BIT_KHR 0x0040
+#endif /* EGL_KHR_config_attribs */
+
+#ifndef EGL_KHR_context_flush_control
+#define EGL_KHR_context_flush_control 1
+#define EGL_CONTEXT_RELEASE_BEHAVIOR_NONE_KHR 0
+#define EGL_CONTEXT_RELEASE_BEHAVIOR_KHR 0x2097
+#define EGL_CONTEXT_RELEASE_BEHAVIOR_FLUSH_KHR 0x2098
+#endif /* EGL_KHR_context_flush_control */
+
+#ifndef EGL_KHR_create_context
+#define EGL_KHR_create_context 1
+#define EGL_CONTEXT_MAJOR_VERSION_KHR 0x3098
+#define EGL_CONTEXT_MINOR_VERSION_KHR 0x30FB
+#define EGL_CONTEXT_FLAGS_KHR 0x30FC
+#define EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR 0x30FD
+#define EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR 0x31BD
+#define EGL_NO_RESET_NOTIFICATION_KHR 0x31BE
+#define EGL_LOSE_CONTEXT_ON_RESET_KHR 0x31BF
+#define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0x00000001
+#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR 0x00000002
+#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR 0x00000004
+#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR 0x00000001
+#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR 0x00000002
+#define EGL_OPENGL_ES3_BIT_KHR 0x00000040
+#endif /* EGL_KHR_create_context */
+
+#ifndef EGL_KHR_create_context_no_error
+#define EGL_KHR_create_context_no_error 1
+#define EGL_CONTEXT_OPENGL_NO_ERROR_KHR 0x31B3
+#endif /* EGL_KHR_create_context_no_error */
+
+#ifndef EGL_KHR_debug
+#define EGL_KHR_debug 1
+typedef void *EGLLabelKHR;
+typedef void *EGLObjectKHR;
+typedef void (EGLAPIENTRY *EGLDEBUGPROCKHR)(EGLenum error,const char *command,EGLint messageType,EGLLabelKHR threadLabel,EGLLabelKHR objectLabel,const char* message);
+#define EGL_OBJECT_THREAD_KHR 0x33B0
+#define EGL_OBJECT_DISPLAY_KHR 0x33B1
+#define EGL_OBJECT_CONTEXT_KHR 0x33B2
+#define EGL_OBJECT_SURFACE_KHR 0x33B3
+#define EGL_OBJECT_IMAGE_KHR 0x33B4
+#define EGL_OBJECT_SYNC_KHR 0x33B5
+#define EGL_OBJECT_STREAM_KHR 0x33B6
+#define EGL_DEBUG_MSG_CRITICAL_KHR 0x33B9
+#define EGL_DEBUG_MSG_ERROR_KHR 0x33BA
+#define EGL_DEBUG_MSG_WARN_KHR 0x33BB
+#define EGL_DEBUG_MSG_INFO_KHR 0x33BC
+#define EGL_DEBUG_CALLBACK_KHR 0x33B8
+typedef EGLint (EGLAPIENTRYP PFNEGLDEBUGMESSAGECONTROLKHRPROC) (EGLDEBUGPROCKHR callback, const EGLAttrib *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDEBUGKHRPROC) (EGLint attribute, EGLAttrib *value);
+typedef EGLint (EGLAPIENTRYP PFNEGLLABELOBJECTKHRPROC) (EGLDisplay display, EGLenum objectType, EGLObjectKHR object, EGLLabelKHR label);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLint EGLAPIENTRY eglDebugMessageControlKHR (EGLDEBUGPROCKHR callback, const EGLAttrib *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDebugKHR (EGLint attribute, EGLAttrib *value);
+EGLAPI EGLint EGLAPIENTRY eglLabelObjectKHR (EGLDisplay display, EGLenum objectType, EGLObjectKHR object, EGLLabelKHR label);
+#endif
+#endif /* EGL_KHR_debug */
+
+#ifndef EGL_KHR_display_reference
+#define EGL_KHR_display_reference 1
+#define EGL_TRACK_REFERENCES_KHR 0x3352
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDISPLAYATTRIBKHRPROC) (EGLDisplay dpy, EGLint name, EGLAttrib *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDisplayAttribKHR (EGLDisplay dpy, EGLint name, EGLAttrib *value);
+#endif
+#endif /* EGL_KHR_display_reference */
+
+#ifndef EGL_KHR_fence_sync
+#define EGL_KHR_fence_sync 1
+typedef khronos_utime_nanoseconds_t EGLTimeKHR;
+#ifdef KHRONOS_SUPPORT_INT64
+#define EGL_SYNC_PRIOR_COMMANDS_COMPLETE_KHR 0x30F0
+#define EGL_SYNC_CONDITION_KHR 0x30F8
+#define EGL_SYNC_FENCE_KHR 0x30F9
+typedef EGLSyncKHR (EGLAPIENTRYP PFNEGLCREATESYNCKHRPROC) (EGLDisplay dpy, EGLenum type, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYSYNCKHRPROC) (EGLDisplay dpy, EGLSyncKHR sync);
+typedef EGLint (EGLAPIENTRYP PFNEGLCLIENTWAITSYNCKHRPROC) (EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETSYNCATTRIBKHRPROC) (EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLSyncKHR EGLAPIENTRY eglCreateSyncKHR (EGLDisplay dpy, EGLenum type, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroySyncKHR (EGLDisplay dpy, EGLSyncKHR sync);
+EGLAPI EGLint EGLAPIENTRY eglClientWaitSyncKHR (EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetSyncAttribKHR (EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint *value);
+#endif
+#endif /* KHRONOS_SUPPORT_INT64 */
+#endif /* EGL_KHR_fence_sync */
+
+#ifndef EGL_KHR_get_all_proc_addresses
+#define EGL_KHR_get_all_proc_addresses 1
+#endif /* EGL_KHR_get_all_proc_addresses */
+
+#ifndef EGL_KHR_gl_colorspace
+#define EGL_KHR_gl_colorspace 1
+#define EGL_GL_COLORSPACE_KHR 0x309D
+#define EGL_GL_COLORSPACE_SRGB_KHR 0x3089
+#define EGL_GL_COLORSPACE_LINEAR_KHR 0x308A
+#endif /* EGL_KHR_gl_colorspace */
+
+#ifndef EGL_KHR_gl_renderbuffer_image
+#define EGL_KHR_gl_renderbuffer_image 1
+#define EGL_GL_RENDERBUFFER_KHR 0x30B9
+#endif /* EGL_KHR_gl_renderbuffer_image */
+
+#ifndef EGL_KHR_gl_texture_2D_image
+#define EGL_KHR_gl_texture_2D_image 1
+#define EGL_GL_TEXTURE_2D_KHR 0x30B1
+#define EGL_GL_TEXTURE_LEVEL_KHR 0x30BC
+#endif /* EGL_KHR_gl_texture_2D_image */
+
+#ifndef EGL_KHR_gl_texture_3D_image
+#define EGL_KHR_gl_texture_3D_image 1
+#define EGL_GL_TEXTURE_3D_KHR 0x30B2
+#define EGL_GL_TEXTURE_ZOFFSET_KHR 0x30BD
+#endif /* EGL_KHR_gl_texture_3D_image */
+
+#ifndef EGL_KHR_gl_texture_cubemap_image
+#define EGL_KHR_gl_texture_cubemap_image 1
+#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X_KHR 0x30B3
+#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X_KHR 0x30B4
+#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y_KHR 0x30B5
+#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_KHR 0x30B6
+#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z_KHR 0x30B7
+#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_KHR 0x30B8
+#endif /* EGL_KHR_gl_texture_cubemap_image */
+
+#ifndef EGL_KHR_image
+#define EGL_KHR_image 1
+typedef void *EGLImageKHR;
+#define EGL_NATIVE_PIXMAP_KHR 0x30B0
+#define EGL_NO_IMAGE_KHR EGL_CAST(EGLImageKHR,0)
+typedef EGLImageKHR (EGLAPIENTRYP PFNEGLCREATEIMAGEKHRPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEKHRPROC) (EGLDisplay dpy, EGLImageKHR image);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLImageKHR EGLAPIENTRY eglCreateImageKHR (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroyImageKHR (EGLDisplay dpy, EGLImageKHR image);
+#endif
+#endif /* EGL_KHR_image */
+
+#ifndef EGL_KHR_image_base
+#define EGL_KHR_image_base 1
+#define EGL_IMAGE_PRESERVED_KHR 0x30D2
+#endif /* EGL_KHR_image_base */
+
+#ifndef EGL_KHR_image_pixmap
+#define EGL_KHR_image_pixmap 1
+#endif /* EGL_KHR_image_pixmap */
+
+#ifndef EGL_KHR_lock_surface
+#define EGL_KHR_lock_surface 1
+#define EGL_READ_SURFACE_BIT_KHR 0x0001
+#define EGL_WRITE_SURFACE_BIT_KHR 0x0002
+#define EGL_LOCK_SURFACE_BIT_KHR 0x0080
+#define EGL_OPTIMAL_FORMAT_BIT_KHR 0x0100
+#define EGL_MATCH_FORMAT_KHR 0x3043
+#define EGL_FORMAT_RGB_565_EXACT_KHR 0x30C0
+#define EGL_FORMAT_RGB_565_KHR 0x30C1
+#define EGL_FORMAT_RGBA_8888_EXACT_KHR 0x30C2
+#define EGL_FORMAT_RGBA_8888_KHR 0x30C3
+#define EGL_MAP_PRESERVE_PIXELS_KHR 0x30C4
+#define EGL_LOCK_USAGE_HINT_KHR 0x30C5
+#define EGL_BITMAP_POINTER_KHR 0x30C6
+#define EGL_BITMAP_PITCH_KHR 0x30C7
+#define EGL_BITMAP_ORIGIN_KHR 0x30C8
+#define EGL_BITMAP_PIXEL_RED_OFFSET_KHR 0x30C9
+#define EGL_BITMAP_PIXEL_GREEN_OFFSET_KHR 0x30CA
+#define EGL_BITMAP_PIXEL_BLUE_OFFSET_KHR 0x30CB
+#define EGL_BITMAP_PIXEL_ALPHA_OFFSET_KHR 0x30CC
+#define EGL_BITMAP_PIXEL_LUMINANCE_OFFSET_KHR 0x30CD
+#define EGL_LOWER_LEFT_KHR 0x30CE
+#define EGL_UPPER_LEFT_KHR 0x30CF
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLLOCKSURFACEKHRPROC) (EGLDisplay dpy, EGLSurface surface, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLUNLOCKSURFACEKHRPROC) (EGLDisplay dpy, EGLSurface surface);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglLockSurfaceKHR (EGLDisplay dpy, EGLSurface surface, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglUnlockSurfaceKHR (EGLDisplay dpy, EGLSurface surface);
+#endif
+#endif /* EGL_KHR_lock_surface */
+
+#ifndef EGL_KHR_lock_surface2
+#define EGL_KHR_lock_surface2 1
+#define EGL_BITMAP_PIXEL_SIZE_KHR 0x3110
+#endif /* EGL_KHR_lock_surface2 */
+
+#ifndef EGL_KHR_lock_surface3
+#define EGL_KHR_lock_surface3 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSURFACE64KHRPROC) (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLAttribKHR *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQuerySurface64KHR (EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLAttribKHR *value);
+#endif
+#endif /* EGL_KHR_lock_surface3 */
+
+#ifndef EGL_KHR_mutable_render_buffer
+#define EGL_KHR_mutable_render_buffer 1
+#define EGL_MUTABLE_RENDER_BUFFER_BIT_KHR 0x1000
+#endif /* EGL_KHR_mutable_render_buffer */
+
+#ifndef EGL_KHR_no_config_context
+#define EGL_KHR_no_config_context 1
+#define EGL_NO_CONFIG_KHR EGL_CAST(EGLConfig,0)
+#endif /* EGL_KHR_no_config_context */
+
+#ifndef EGL_KHR_partial_update
+#define EGL_KHR_partial_update 1
+#define EGL_BUFFER_AGE_KHR 0x313D
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSETDAMAGEREGIONKHRPROC) (EGLDisplay dpy, EGLSurface surface, EGLint *rects, EGLint n_rects);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglSetDamageRegionKHR (EGLDisplay dpy, EGLSurface surface, EGLint *rects, EGLint n_rects);
+#endif
+#endif /* EGL_KHR_partial_update */
+
+#ifndef EGL_KHR_platform_android
+#define EGL_KHR_platform_android 1
+#define EGL_PLATFORM_ANDROID_KHR 0x3141
+#endif /* EGL_KHR_platform_android */
+
+#ifndef EGL_KHR_platform_gbm
+#define EGL_KHR_platform_gbm 1
+#define EGL_PLATFORM_GBM_KHR 0x31D7
+#endif /* EGL_KHR_platform_gbm */
+
+#ifndef EGL_KHR_platform_wayland
+#define EGL_KHR_platform_wayland 1
+#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
+#endif /* EGL_KHR_platform_wayland */
+
+#ifndef EGL_KHR_platform_x11
+#define EGL_KHR_platform_x11 1
+#define EGL_PLATFORM_X11_KHR 0x31D5
+#define EGL_PLATFORM_X11_SCREEN_KHR 0x31D6
+#endif /* EGL_KHR_platform_x11 */
+
+#ifndef EGL_KHR_reusable_sync
+#define EGL_KHR_reusable_sync 1
+#ifdef KHRONOS_SUPPORT_INT64
+#define EGL_SYNC_STATUS_KHR 0x30F1
+#define EGL_SIGNALED_KHR 0x30F2
+#define EGL_UNSIGNALED_KHR 0x30F3
+#define EGL_TIMEOUT_EXPIRED_KHR 0x30F5
+#define EGL_CONDITION_SATISFIED_KHR 0x30F6
+#define EGL_SYNC_TYPE_KHR 0x30F7
+#define EGL_SYNC_REUSABLE_KHR 0x30FA
+#define EGL_SYNC_FLUSH_COMMANDS_BIT_KHR 0x0001
+#define EGL_FOREVER_KHR 0xFFFFFFFFFFFFFFFFull
+#define EGL_NO_SYNC_KHR EGL_CAST(EGLSyncKHR,0)
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSIGNALSYNCKHRPROC) (EGLDisplay dpy, EGLSyncKHR sync, EGLenum mode);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglSignalSyncKHR (EGLDisplay dpy, EGLSyncKHR sync, EGLenum mode);
+#endif
+#endif /* KHRONOS_SUPPORT_INT64 */
+#endif /* EGL_KHR_reusable_sync */
+
+#ifndef EGL_KHR_stream
+#define EGL_KHR_stream 1
+typedef void *EGLStreamKHR;
+typedef khronos_uint64_t EGLuint64KHR;
+#ifdef KHRONOS_SUPPORT_INT64
+#define EGL_NO_STREAM_KHR EGL_CAST(EGLStreamKHR,0)
+#define EGL_CONSUMER_LATENCY_USEC_KHR 0x3210
+#define EGL_PRODUCER_FRAME_KHR 0x3212
+#define EGL_CONSUMER_FRAME_KHR 0x3213
+#define EGL_STREAM_STATE_KHR 0x3214
+#define EGL_STREAM_STATE_CREATED_KHR 0x3215
+#define EGL_STREAM_STATE_CONNECTING_KHR 0x3216
+#define EGL_STREAM_STATE_EMPTY_KHR 0x3217
+#define EGL_STREAM_STATE_NEW_FRAME_AVAILABLE_KHR 0x3218
+#define EGL_STREAM_STATE_OLD_FRAME_AVAILABLE_KHR 0x3219
+#define EGL_STREAM_STATE_DISCONNECTED_KHR 0x321A
+#define EGL_BAD_STREAM_KHR 0x321B
+#define EGL_BAD_STATE_KHR 0x321C
+typedef EGLStreamKHR (EGLAPIENTRYP PFNEGLCREATESTREAMKHRPROC) (EGLDisplay dpy, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYSTREAMKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMATTRIBKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLint value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSTREAMKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLint *value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSTREAMU64KHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLuint64KHR *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLStreamKHR EGLAPIENTRY eglCreateStreamKHR (EGLDisplay dpy, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroyStreamKHR (EGLDisplay dpy, EGLStreamKHR stream);
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamAttribKHR (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLint value);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryStreamKHR (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLint *value);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryStreamu64KHR (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLuint64KHR *value);
+#endif
+#endif /* KHRONOS_SUPPORT_INT64 */
+#endif /* EGL_KHR_stream */
+
+#ifndef EGL_KHR_stream_attrib
+#define EGL_KHR_stream_attrib 1
+#ifdef KHRONOS_SUPPORT_INT64
+typedef EGLStreamKHR (EGLAPIENTRYP PFNEGLCREATESTREAMATTRIBKHRPROC) (EGLDisplay dpy, const EGLAttrib *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSETSTREAMATTRIBKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLAttrib value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSTREAMATTRIBKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLAttrib *value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMCONSUMERACQUIREATTRIBKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMCONSUMERRELEASEATTRIBKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLStreamKHR EGLAPIENTRY eglCreateStreamAttribKHR (EGLDisplay dpy, const EGLAttrib *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglSetStreamAttribKHR (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLAttrib value);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryStreamAttribKHR (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLAttrib *value);
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamConsumerAcquireAttribKHR (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamConsumerReleaseAttribKHR (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+#endif
+#endif /* KHRONOS_SUPPORT_INT64 */
+#endif /* EGL_KHR_stream_attrib */
+
+#ifndef EGL_KHR_stream_consumer_gltexture
+#define EGL_KHR_stream_consumer_gltexture 1
+#ifdef EGL_KHR_stream
+#define EGL_CONSUMER_ACQUIRE_TIMEOUT_USEC_KHR 0x321E
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMCONSUMERACQUIREKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMCONSUMERRELEASEKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamConsumerGLTextureExternalKHR (EGLDisplay dpy, EGLStreamKHR stream);
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamConsumerAcquireKHR (EGLDisplay dpy, EGLStreamKHR stream);
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamConsumerReleaseKHR (EGLDisplay dpy, EGLStreamKHR stream);
+#endif
+#endif /* EGL_KHR_stream */
+#endif /* EGL_KHR_stream_consumer_gltexture */
+
+#ifndef EGL_KHR_stream_cross_process_fd
+#define EGL_KHR_stream_cross_process_fd 1
+typedef int EGLNativeFileDescriptorKHR;
+#ifdef EGL_KHR_stream
+#define EGL_NO_FILE_DESCRIPTOR_KHR EGL_CAST(EGLNativeFileDescriptorKHR,-1)
+typedef EGLNativeFileDescriptorKHR (EGLAPIENTRYP PFNEGLGETSTREAMFILEDESCRIPTORKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream);
+typedef EGLStreamKHR (EGLAPIENTRYP PFNEGLCREATESTREAMFROMFILEDESCRIPTORKHRPROC) (EGLDisplay dpy, EGLNativeFileDescriptorKHR file_descriptor);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLNativeFileDescriptorKHR EGLAPIENTRY eglGetStreamFileDescriptorKHR (EGLDisplay dpy, EGLStreamKHR stream);
+EGLAPI EGLStreamKHR EGLAPIENTRY eglCreateStreamFromFileDescriptorKHR (EGLDisplay dpy, EGLNativeFileDescriptorKHR file_descriptor);
+#endif
+#endif /* EGL_KHR_stream */
+#endif /* EGL_KHR_stream_cross_process_fd */
+
+#ifndef EGL_KHR_stream_fifo
+#define EGL_KHR_stream_fifo 1
+#ifdef EGL_KHR_stream
+#define EGL_STREAM_FIFO_LENGTH_KHR 0x31FC
+#define EGL_STREAM_TIME_NOW_KHR 0x31FD
+#define EGL_STREAM_TIME_CONSUMER_KHR 0x31FE
+#define EGL_STREAM_TIME_PRODUCER_KHR 0x31FF
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSTREAMTIMEKHRPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLTimeKHR *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryStreamTimeKHR (EGLDisplay dpy, EGLStreamKHR stream, EGLenum attribute, EGLTimeKHR *value);
+#endif
+#endif /* EGL_KHR_stream */
+#endif /* EGL_KHR_stream_fifo */
+
+#ifndef EGL_KHR_stream_producer_aldatalocator
+#define EGL_KHR_stream_producer_aldatalocator 1
+#ifdef EGL_KHR_stream
+#endif /* EGL_KHR_stream */
+#endif /* EGL_KHR_stream_producer_aldatalocator */
+
+#ifndef EGL_KHR_stream_producer_eglsurface
+#define EGL_KHR_stream_producer_eglsurface 1
+#ifdef EGL_KHR_stream
+#define EGL_STREAM_BIT_KHR 0x0800
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATESTREAMPRODUCERSURFACEKHRPROC) (EGLDisplay dpy, EGLConfig config, EGLStreamKHR stream, const EGLint *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLSurface EGLAPIENTRY eglCreateStreamProducerSurfaceKHR (EGLDisplay dpy, EGLConfig config, EGLStreamKHR stream, const EGLint *attrib_list);
+#endif
+#endif /* EGL_KHR_stream */
+#endif /* EGL_KHR_stream_producer_eglsurface */
+
+#ifndef EGL_KHR_surfaceless_context
+#define EGL_KHR_surfaceless_context 1
+#endif /* EGL_KHR_surfaceless_context */
+
+#ifndef EGL_KHR_swap_buffers_with_damage
+#define EGL_KHR_swap_buffers_with_damage 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC) (EGLDisplay dpy, EGLSurface surface, const EGLint *rects, EGLint n_rects);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglSwapBuffersWithDamageKHR (EGLDisplay dpy, EGLSurface surface, const EGLint *rects, EGLint n_rects);
+#endif
+#endif /* EGL_KHR_swap_buffers_with_damage */
+
+#ifndef EGL_KHR_vg_parent_image
+#define EGL_KHR_vg_parent_image 1
+#define EGL_VG_PARENT_IMAGE_KHR 0x30BA
+#endif /* EGL_KHR_vg_parent_image */
+
+#ifndef EGL_KHR_wait_sync
+#define EGL_KHR_wait_sync 1
+typedef EGLint (EGLAPIENTRYP PFNEGLWAITSYNCKHRPROC) (EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLint EGLAPIENTRY eglWaitSyncKHR (EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
+#endif
+#endif /* EGL_KHR_wait_sync */
+
+#ifndef EGL_ANDROID_GLES_layers
+#define EGL_ANDROID_GLES_layers 1
+#endif /* EGL_ANDROID_GLES_layers */
+
+#ifndef EGL_ANDROID_blob_cache
+#define EGL_ANDROID_blob_cache 1
+typedef khronos_ssize_t EGLsizeiANDROID;
+typedef void (*EGLSetBlobFuncANDROID) (const void *key, EGLsizeiANDROID keySize, const void *value, EGLsizeiANDROID valueSize);
+typedef EGLsizeiANDROID (*EGLGetBlobFuncANDROID) (const void *key, EGLsizeiANDROID keySize, void *value, EGLsizeiANDROID valueSize);
+typedef void (EGLAPIENTRYP PFNEGLSETBLOBCACHEFUNCSANDROIDPROC) (EGLDisplay dpy, EGLSetBlobFuncANDROID set, EGLGetBlobFuncANDROID get);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI void EGLAPIENTRY eglSetBlobCacheFuncsANDROID (EGLDisplay dpy, EGLSetBlobFuncANDROID set, EGLGetBlobFuncANDROID get);
+#endif
+#endif /* EGL_ANDROID_blob_cache */
+
+#ifndef EGL_ANDROID_create_native_client_buffer
+#define EGL_ANDROID_create_native_client_buffer 1
+#define EGL_NATIVE_BUFFER_USAGE_ANDROID 0x3143
+#define EGL_NATIVE_BUFFER_USAGE_PROTECTED_BIT_ANDROID 0x00000001
+#define EGL_NATIVE_BUFFER_USAGE_RENDERBUFFER_BIT_ANDROID 0x00000002
+#define EGL_NATIVE_BUFFER_USAGE_TEXTURE_BIT_ANDROID 0x00000004
+typedef EGLClientBuffer (EGLAPIENTRYP PFNEGLCREATENATIVECLIENTBUFFERANDROIDPROC) (const EGLint *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLClientBuffer EGLAPIENTRY eglCreateNativeClientBufferANDROID (const EGLint *attrib_list);
+#endif
+#endif /* EGL_ANDROID_create_native_client_buffer */
+
+#ifndef EGL_ANDROID_framebuffer_target
+#define EGL_ANDROID_framebuffer_target 1
+#define EGL_FRAMEBUFFER_TARGET_ANDROID 0x3147
+#endif /* EGL_ANDROID_framebuffer_target */
+
+#ifndef EGL_ANDROID_front_buffer_auto_refresh
+#define EGL_ANDROID_front_buffer_auto_refresh 1
+#define EGL_FRONT_BUFFER_AUTO_REFRESH_ANDROID 0x314C
+#endif /* EGL_ANDROID_front_buffer_auto_refresh */
+
+#ifndef EGL_ANDROID_get_frame_timestamps
+#define EGL_ANDROID_get_frame_timestamps 1
+typedef khronos_stime_nanoseconds_t EGLnsecsANDROID;
+#define EGL_TIMESTAMP_PENDING_ANDROID EGL_CAST(EGLnsecsANDROID,-2)
+#define EGL_TIMESTAMP_INVALID_ANDROID EGL_CAST(EGLnsecsANDROID,-1)
+#define EGL_TIMESTAMPS_ANDROID 0x3430
+#define EGL_COMPOSITE_DEADLINE_ANDROID 0x3431
+#define EGL_COMPOSITE_INTERVAL_ANDROID 0x3432
+#define EGL_COMPOSITE_TO_PRESENT_LATENCY_ANDROID 0x3433
+#define EGL_REQUESTED_PRESENT_TIME_ANDROID 0x3434
+#define EGL_RENDERING_COMPLETE_TIME_ANDROID 0x3435
+#define EGL_COMPOSITION_LATCH_TIME_ANDROID 0x3436
+#define EGL_FIRST_COMPOSITION_START_TIME_ANDROID 0x3437
+#define EGL_LAST_COMPOSITION_START_TIME_ANDROID 0x3438
+#define EGL_FIRST_COMPOSITION_GPU_FINISHED_TIME_ANDROID 0x3439
+#define EGL_DISPLAY_PRESENT_TIME_ANDROID 0x343A
+#define EGL_DEQUEUE_READY_TIME_ANDROID 0x343B
+#define EGL_READS_DONE_TIME_ANDROID 0x343C
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETCOMPOSITORTIMINGSUPPORTEDANDROIDPROC) (EGLDisplay dpy, EGLSurface surface, EGLint name);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETCOMPOSITORTIMINGANDROIDPROC) (EGLDisplay dpy, EGLSurface surface, EGLint numTimestamps, const EGLint *names, EGLnsecsANDROID *values);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETNEXTFRAMEIDANDROIDPROC) (EGLDisplay dpy, EGLSurface surface, EGLuint64KHR *frameId);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETFRAMETIMESTAMPSUPPORTEDANDROIDPROC) (EGLDisplay dpy, EGLSurface surface, EGLint timestamp);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETFRAMETIMESTAMPSANDROIDPROC) (EGLDisplay dpy, EGLSurface surface, EGLuint64KHR frameId, EGLint numTimestamps, const EGLint *timestamps, EGLnsecsANDROID *values);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglGetCompositorTimingSupportedANDROID (EGLDisplay dpy, EGLSurface surface, EGLint name);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetCompositorTimingANDROID (EGLDisplay dpy, EGLSurface surface, EGLint numTimestamps, const EGLint *names, EGLnsecsANDROID *values);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetNextFrameIdANDROID (EGLDisplay dpy, EGLSurface surface, EGLuint64KHR *frameId);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetFrameTimestampSupportedANDROID (EGLDisplay dpy, EGLSurface surface, EGLint timestamp);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetFrameTimestampsANDROID (EGLDisplay dpy, EGLSurface surface, EGLuint64KHR frameId, EGLint numTimestamps, const EGLint *timestamps, EGLnsecsANDROID *values);
+#endif
+#endif /* EGL_ANDROID_get_frame_timestamps */
+
+#ifndef EGL_ANDROID_get_native_client_buffer
+#define EGL_ANDROID_get_native_client_buffer 1
+struct AHardwareBuffer;
+typedef EGLClientBuffer (EGLAPIENTRYP PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC) (const struct AHardwareBuffer *buffer);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLClientBuffer EGLAPIENTRY eglGetNativeClientBufferANDROID (const struct AHardwareBuffer *buffer);
+#endif
+#endif /* EGL_ANDROID_get_native_client_buffer */
+
+#ifndef EGL_ANDROID_image_native_buffer
+#define EGL_ANDROID_image_native_buffer 1
+#define EGL_NATIVE_BUFFER_ANDROID 0x3140
+#endif /* EGL_ANDROID_image_native_buffer */
+
+#ifndef EGL_ANDROID_native_fence_sync
+#define EGL_ANDROID_native_fence_sync 1
+#define EGL_SYNC_NATIVE_FENCE_ANDROID 0x3144
+#define EGL_SYNC_NATIVE_FENCE_FD_ANDROID 0x3145
+#define EGL_SYNC_NATIVE_FENCE_SIGNALED_ANDROID 0x3146
+#define EGL_NO_NATIVE_FENCE_FD_ANDROID -1
+typedef EGLint (EGLAPIENTRYP PFNEGLDUPNATIVEFENCEFDANDROIDPROC) (EGLDisplay dpy, EGLSyncKHR sync);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLint EGLAPIENTRY eglDupNativeFenceFDANDROID (EGLDisplay dpy, EGLSyncKHR sync);
+#endif
+#endif /* EGL_ANDROID_native_fence_sync */
+
+#ifndef EGL_ANDROID_presentation_time
+#define EGL_ANDROID_presentation_time 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLPRESENTATIONTIMEANDROIDPROC) (EGLDisplay dpy, EGLSurface surface, EGLnsecsANDROID time);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglPresentationTimeANDROID (EGLDisplay dpy, EGLSurface surface, EGLnsecsANDROID time);
+#endif
+#endif /* EGL_ANDROID_presentation_time */
+
+#ifndef EGL_ANDROID_recordable
+#define EGL_ANDROID_recordable 1
+#define EGL_RECORDABLE_ANDROID 0x3142
+#endif /* EGL_ANDROID_recordable */
+
+#ifndef EGL_ANGLE_d3d_share_handle_client_buffer
+#define EGL_ANGLE_d3d_share_handle_client_buffer 1
+#define EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE 0x3200
+#endif /* EGL_ANGLE_d3d_share_handle_client_buffer */
+
+#ifndef EGL_ANGLE_device_d3d
+#define EGL_ANGLE_device_d3d 1
+#define EGL_D3D9_DEVICE_ANGLE 0x33A0
+#define EGL_D3D11_DEVICE_ANGLE 0x33A1
+#endif /* EGL_ANGLE_device_d3d */
+
+#ifndef EGL_ANGLE_query_surface_pointer
+#define EGL_ANGLE_query_surface_pointer 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSURFACEPOINTERANGLEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint attribute, void **value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQuerySurfacePointerANGLE (EGLDisplay dpy, EGLSurface surface, EGLint attribute, void **value);
+#endif
+#endif /* EGL_ANGLE_query_surface_pointer */
+
+#ifndef EGL_ANGLE_surface_d3d_texture_2d_share_handle
+#define EGL_ANGLE_surface_d3d_texture_2d_share_handle 1
+#endif /* EGL_ANGLE_surface_d3d_texture_2d_share_handle */
+
+#ifndef EGL_ANGLE_sync_control_rate
+#define EGL_ANGLE_sync_control_rate 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETMSCRATEANGLEPROC) (EGLDisplay dpy, EGLSurface surface, EGLint *numerator, EGLint *denominator);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglGetMscRateANGLE (EGLDisplay dpy, EGLSurface surface, EGLint *numerator, EGLint *denominator);
+#endif
+#endif /* EGL_ANGLE_sync_control_rate */
+
+#ifndef EGL_ANGLE_window_fixed_size
+#define EGL_ANGLE_window_fixed_size 1
+#define EGL_FIXED_SIZE_ANGLE 0x3201
+#endif /* EGL_ANGLE_window_fixed_size */
+
+#ifndef EGL_ARM_image_format
+#define EGL_ARM_image_format 1
+#define EGL_COLOR_COMPONENT_TYPE_UNSIGNED_INTEGER_ARM 0x3287
+#define EGL_COLOR_COMPONENT_TYPE_INTEGER_ARM 0x3288
+#endif /* EGL_ARM_image_format */
+
+#ifndef EGL_ARM_implicit_external_sync
+#define EGL_ARM_implicit_external_sync 1
+#define EGL_SYNC_PRIOR_COMMANDS_IMPLICIT_EXTERNAL_ARM 0x328A
+#endif /* EGL_ARM_implicit_external_sync */
+
+#ifndef EGL_ARM_pixmap_multisample_discard
+#define EGL_ARM_pixmap_multisample_discard 1
+#define EGL_DISCARD_SAMPLES_ARM 0x3286
+#endif /* EGL_ARM_pixmap_multisample_discard */
+
+#ifndef EGL_EXT_bind_to_front
+#define EGL_EXT_bind_to_front 1
+#define EGL_FRONT_BUFFER_EXT 0x3464
+#endif /* EGL_EXT_bind_to_front */
+
+#ifndef EGL_EXT_buffer_age
+#define EGL_EXT_buffer_age 1
+#define EGL_BUFFER_AGE_EXT 0x313D
+#endif /* EGL_EXT_buffer_age */
+
+#ifndef EGL_EXT_client_extensions
+#define EGL_EXT_client_extensions 1
+#endif /* EGL_EXT_client_extensions */
+
+#ifndef EGL_EXT_client_sync
+#define EGL_EXT_client_sync 1
+#define EGL_SYNC_CLIENT_EXT 0x3364
+#define EGL_SYNC_CLIENT_SIGNAL_EXT 0x3365
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCLIENTSIGNALSYNCEXTPROC) (EGLDisplay dpy, EGLSync sync, const EGLAttrib *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglClientSignalSyncEXT (EGLDisplay dpy, EGLSync sync, const EGLAttrib *attrib_list);
+#endif
+#endif /* EGL_EXT_client_sync */
+
+#ifndef EGL_EXT_compositor
+#define EGL_EXT_compositor 1
+#define EGL_PRIMARY_COMPOSITOR_CONTEXT_EXT 0x3460
+#define EGL_EXTERNAL_REF_ID_EXT 0x3461
+#define EGL_COMPOSITOR_DROP_NEWEST_FRAME_EXT 0x3462
+#define EGL_COMPOSITOR_KEEP_NEWEST_FRAME_EXT 0x3463
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOMPOSITORSETCONTEXTLISTEXTPROC) (const EGLint *external_ref_ids, EGLint num_entries);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOMPOSITORSETCONTEXTATTRIBUTESEXTPROC) (EGLint external_ref_id, const EGLint *context_attributes, EGLint num_entries);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOMPOSITORSETWINDOWLISTEXTPROC) (EGLint external_ref_id, const EGLint *external_win_ids, EGLint num_entries);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOMPOSITORSETWINDOWATTRIBUTESEXTPROC) (EGLint external_win_id, const EGLint *window_attributes, EGLint num_entries);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOMPOSITORBINDTEXWINDOWEXTPROC) (EGLint external_win_id);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOMPOSITORSETSIZEEXTPROC) (EGLint external_win_id, EGLint width, EGLint height);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLCOMPOSITORSWAPPOLICYEXTPROC) (EGLint external_win_id, EGLint policy);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglCompositorSetContextListEXT (const EGLint *external_ref_ids, EGLint num_entries);
+EGLAPI EGLBoolean EGLAPIENTRY eglCompositorSetContextAttributesEXT (EGLint external_ref_id, const EGLint *context_attributes, EGLint num_entries);
+EGLAPI EGLBoolean EGLAPIENTRY eglCompositorSetWindowListEXT (EGLint external_ref_id, const EGLint *external_win_ids, EGLint num_entries);
+EGLAPI EGLBoolean EGLAPIENTRY eglCompositorSetWindowAttributesEXT (EGLint external_win_id, const EGLint *window_attributes, EGLint num_entries);
+EGLAPI EGLBoolean EGLAPIENTRY eglCompositorBindTexWindowEXT (EGLint external_win_id);
+EGLAPI EGLBoolean EGLAPIENTRY eglCompositorSetSizeEXT (EGLint external_win_id, EGLint width, EGLint height);
+EGLAPI EGLBoolean EGLAPIENTRY eglCompositorSwapPolicyEXT (EGLint external_win_id, EGLint policy);
+#endif
+#endif /* EGL_EXT_compositor */
+
+#ifndef EGL_EXT_config_select_group
+#define EGL_EXT_config_select_group 1
+#define EGL_CONFIG_SELECT_GROUP_EXT 0x34C0
+#endif /* EGL_EXT_config_select_group */
+
+#ifndef EGL_EXT_create_context_robustness
+#define EGL_EXT_create_context_robustness 1
+#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT 0x30BF
+#define EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT 0x3138
+#define EGL_NO_RESET_NOTIFICATION_EXT 0x31BE
+#define EGL_LOSE_CONTEXT_ON_RESET_EXT 0x31BF
+#endif /* EGL_EXT_create_context_robustness */
+
+#ifndef EGL_EXT_device_base
+#define EGL_EXT_device_base 1
+typedef void *EGLDeviceEXT;
+#define EGL_NO_DEVICE_EXT EGL_CAST(EGLDeviceEXT,0)
+#define EGL_BAD_DEVICE_EXT 0x322B
+#define EGL_DEVICE_EXT 0x322C
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDEVICEATTRIBEXTPROC) (EGLDeviceEXT device, EGLint attribute, EGLAttrib *value);
+typedef const char *(EGLAPIENTRYP PFNEGLQUERYDEVICESTRINGEXTPROC) (EGLDeviceEXT device, EGLint name);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDEVICESEXTPROC) (EGLint max_devices, EGLDeviceEXT *devices, EGLint *num_devices);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDISPLAYATTRIBEXTPROC) (EGLDisplay dpy, EGLint attribute, EGLAttrib *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDeviceAttribEXT (EGLDeviceEXT device, EGLint attribute, EGLAttrib *value);
+EGLAPI const char *EGLAPIENTRY eglQueryDeviceStringEXT (EGLDeviceEXT device, EGLint name);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDevicesEXT (EGLint max_devices, EGLDeviceEXT *devices, EGLint *num_devices);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDisplayAttribEXT (EGLDisplay dpy, EGLint attribute, EGLAttrib *value);
+#endif
+#endif /* EGL_EXT_device_base */
+
+#ifndef EGL_EXT_device_drm
+#define EGL_EXT_device_drm 1
+#define EGL_DRM_DEVICE_FILE_EXT 0x3233
+#define EGL_DRM_MASTER_FD_EXT 0x333C
+#endif /* EGL_EXT_device_drm */
+
+#ifndef EGL_EXT_device_drm_render_node
+#define EGL_EXT_device_drm_render_node 1
+#define EGL_DRM_RENDER_NODE_FILE_EXT 0x3377
+#endif /* EGL_EXT_device_drm_render_node */
+
+#ifndef EGL_EXT_device_enumeration
+#define EGL_EXT_device_enumeration 1
+#endif /* EGL_EXT_device_enumeration */
+
+#ifndef EGL_EXT_device_openwf
+#define EGL_EXT_device_openwf 1
+#define EGL_OPENWF_DEVICE_ID_EXT 0x3237
+#define EGL_OPENWF_DEVICE_EXT 0x333D
+#endif /* EGL_EXT_device_openwf */
+
+#ifndef EGL_EXT_device_persistent_id
+#define EGL_EXT_device_persistent_id 1
+#define EGL_DEVICE_UUID_EXT 0x335C
+#define EGL_DRIVER_UUID_EXT 0x335D
+#define EGL_DRIVER_NAME_EXT 0x335E
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDEVICEBINARYEXTPROC) (EGLDeviceEXT device, EGLint name, EGLint max_size, void *value, EGLint *size);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDeviceBinaryEXT (EGLDeviceEXT device, EGLint name, EGLint max_size, void *value, EGLint *size);
+#endif
+#endif /* EGL_EXT_device_persistent_id */
+
+#ifndef EGL_EXT_device_query
+#define EGL_EXT_device_query 1
+#endif /* EGL_EXT_device_query */
+
+#ifndef EGL_EXT_device_query_name
+#define EGL_EXT_device_query_name 1
+#define EGL_RENDERER_EXT 0x335F
+#endif /* EGL_EXT_device_query_name */
+
+#ifndef EGL_EXT_explicit_device
+#define EGL_EXT_explicit_device 1
+#endif /* EGL_EXT_explicit_device */
+
+#ifndef EGL_EXT_gl_colorspace_bt2020_linear
+#define EGL_EXT_gl_colorspace_bt2020_linear 1
+#define EGL_GL_COLORSPACE_BT2020_LINEAR_EXT 0x333F
+#endif /* EGL_EXT_gl_colorspace_bt2020_linear */
+
+#ifndef EGL_EXT_gl_colorspace_bt2020_pq
+#define EGL_EXT_gl_colorspace_bt2020_pq 1
+#define EGL_GL_COLORSPACE_BT2020_PQ_EXT 0x3340
+#endif /* EGL_EXT_gl_colorspace_bt2020_pq */
+
+#ifndef EGL_EXT_gl_colorspace_display_p3
+#define EGL_EXT_gl_colorspace_display_p3 1
+#define EGL_GL_COLORSPACE_DISPLAY_P3_EXT 0x3363
+#endif /* EGL_EXT_gl_colorspace_display_p3 */
+
+#ifndef EGL_EXT_gl_colorspace_display_p3_linear
+#define EGL_EXT_gl_colorspace_display_p3_linear 1
+#define EGL_GL_COLORSPACE_DISPLAY_P3_LINEAR_EXT 0x3362
+#endif /* EGL_EXT_gl_colorspace_display_p3_linear */
+
+#ifndef EGL_EXT_gl_colorspace_display_p3_passthrough
+#define EGL_EXT_gl_colorspace_display_p3_passthrough 1
+#define EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT 0x3490
+#endif /* EGL_EXT_gl_colorspace_display_p3_passthrough */
+
+#ifndef EGL_EXT_gl_colorspace_scrgb
+#define EGL_EXT_gl_colorspace_scrgb 1
+#define EGL_GL_COLORSPACE_SCRGB_EXT 0x3351
+#endif /* EGL_EXT_gl_colorspace_scrgb */
+
+#ifndef EGL_EXT_gl_colorspace_scrgb_linear
+#define EGL_EXT_gl_colorspace_scrgb_linear 1
+#define EGL_GL_COLORSPACE_SCRGB_LINEAR_EXT 0x3350
+#endif /* EGL_EXT_gl_colorspace_scrgb_linear */
+
+#ifndef EGL_EXT_image_dma_buf_import
+#define EGL_EXT_image_dma_buf_import 1
+#define EGL_LINUX_DMA_BUF_EXT 0x3270
+#define EGL_LINUX_DRM_FOURCC_EXT 0x3271
+#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272
+#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273
+#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274
+#define EGL_DMA_BUF_PLANE1_FD_EXT 0x3275
+#define EGL_DMA_BUF_PLANE1_OFFSET_EXT 0x3276
+#define EGL_DMA_BUF_PLANE1_PITCH_EXT 0x3277
+#define EGL_DMA_BUF_PLANE2_FD_EXT 0x3278
+#define EGL_DMA_BUF_PLANE2_OFFSET_EXT 0x3279
+#define EGL_DMA_BUF_PLANE2_PITCH_EXT 0x327A
+#define EGL_YUV_COLOR_SPACE_HINT_EXT 0x327B
+#define EGL_SAMPLE_RANGE_HINT_EXT 0x327C
+#define EGL_YUV_CHROMA_HORIZONTAL_SITING_HINT_EXT 0x327D
+#define EGL_YUV_CHROMA_VERTICAL_SITING_HINT_EXT 0x327E
+#define EGL_ITU_REC601_EXT 0x327F
+#define EGL_ITU_REC709_EXT 0x3280
+#define EGL_ITU_REC2020_EXT 0x3281
+#define EGL_YUV_FULL_RANGE_EXT 0x3282
+#define EGL_YUV_NARROW_RANGE_EXT 0x3283
+#define EGL_YUV_CHROMA_SITING_0_EXT 0x3284
+#define EGL_YUV_CHROMA_SITING_0_5_EXT 0x3285
+#endif /* EGL_EXT_image_dma_buf_import */
+
+#ifndef EGL_EXT_image_dma_buf_import_modifiers
+#define EGL_EXT_image_dma_buf_import_modifiers 1
+#define EGL_DMA_BUF_PLANE3_FD_EXT 0x3440
+#define EGL_DMA_BUF_PLANE3_OFFSET_EXT 0x3441
+#define EGL_DMA_BUF_PLANE3_PITCH_EXT 0x3442
+#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443
+#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444
+#define EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT 0x3445
+#define EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT 0x3446
+#define EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT 0x3447
+#define EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT 0x3448
+#define EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT 0x3449
+#define EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT 0x344A
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDMABUFFORMATSEXTPROC) (EGLDisplay dpy, EGLint max_formats, EGLint *formats, EGLint *num_formats);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDMABUFMODIFIERSEXTPROC) (EGLDisplay dpy, EGLint format, EGLint max_modifiers, EGLuint64KHR *modifiers, EGLBoolean *external_only, EGLint *num_modifiers);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDmaBufFormatsEXT (EGLDisplay dpy, EGLint max_formats, EGLint *formats, EGLint *num_formats);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDmaBufModifiersEXT (EGLDisplay dpy, EGLint format, EGLint max_modifiers, EGLuint64KHR *modifiers, EGLBoolean *external_only, EGLint *num_modifiers);
+#endif
+#endif /* EGL_EXT_image_dma_buf_import_modifiers */
+
+#ifndef EGL_EXT_image_gl_colorspace
+#define EGL_EXT_image_gl_colorspace 1
+#define EGL_GL_COLORSPACE_DEFAULT_EXT 0x314D
+#endif /* EGL_EXT_image_gl_colorspace */
+
+#ifndef EGL_EXT_image_implicit_sync_control
+#define EGL_EXT_image_implicit_sync_control 1
+#define EGL_IMPORT_SYNC_TYPE_EXT 0x3470
+#define EGL_IMPORT_IMPLICIT_SYNC_EXT 0x3471
+#define EGL_IMPORT_EXPLICIT_SYNC_EXT 0x3472
+#endif /* EGL_EXT_image_implicit_sync_control */
+
+#ifndef EGL_EXT_multiview_window
+#define EGL_EXT_multiview_window 1
+#define EGL_MULTIVIEW_VIEW_COUNT_EXT 0x3134
+#endif /* EGL_EXT_multiview_window */
+
+#ifndef EGL_EXT_output_base
+#define EGL_EXT_output_base 1
+typedef void *EGLOutputLayerEXT;
+typedef void *EGLOutputPortEXT;
+#define EGL_NO_OUTPUT_LAYER_EXT EGL_CAST(EGLOutputLayerEXT,0)
+#define EGL_NO_OUTPUT_PORT_EXT EGL_CAST(EGLOutputPortEXT,0)
+#define EGL_BAD_OUTPUT_LAYER_EXT 0x322D
+#define EGL_BAD_OUTPUT_PORT_EXT 0x322E
+#define EGL_SWAP_INTERVAL_EXT 0x322F
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETOUTPUTLAYERSEXTPROC) (EGLDisplay dpy, const EGLAttrib *attrib_list, EGLOutputLayerEXT *layers, EGLint max_layers, EGLint *num_layers);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETOUTPUTPORTSEXTPROC) (EGLDisplay dpy, const EGLAttrib *attrib_list, EGLOutputPortEXT *ports, EGLint max_ports, EGLint *num_ports);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLOUTPUTLAYERATTRIBEXTPROC) (EGLDisplay dpy, EGLOutputLayerEXT layer, EGLint attribute, EGLAttrib value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYOUTPUTLAYERATTRIBEXTPROC) (EGLDisplay dpy, EGLOutputLayerEXT layer, EGLint attribute, EGLAttrib *value);
+typedef const char *(EGLAPIENTRYP PFNEGLQUERYOUTPUTLAYERSTRINGEXTPROC) (EGLDisplay dpy, EGLOutputLayerEXT layer, EGLint name);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLOUTPUTPORTATTRIBEXTPROC) (EGLDisplay dpy, EGLOutputPortEXT port, EGLint attribute, EGLAttrib value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYOUTPUTPORTATTRIBEXTPROC) (EGLDisplay dpy, EGLOutputPortEXT port, EGLint attribute, EGLAttrib *value);
+typedef const char *(EGLAPIENTRYP PFNEGLQUERYOUTPUTPORTSTRINGEXTPROC) (EGLDisplay dpy, EGLOutputPortEXT port, EGLint name);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglGetOutputLayersEXT (EGLDisplay dpy, const EGLAttrib *attrib_list, EGLOutputLayerEXT *layers, EGLint max_layers, EGLint *num_layers);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetOutputPortsEXT (EGLDisplay dpy, const EGLAttrib *attrib_list, EGLOutputPortEXT *ports, EGLint max_ports, EGLint *num_ports);
+EGLAPI EGLBoolean EGLAPIENTRY eglOutputLayerAttribEXT (EGLDisplay dpy, EGLOutputLayerEXT layer, EGLint attribute, EGLAttrib value);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryOutputLayerAttribEXT (EGLDisplay dpy, EGLOutputLayerEXT layer, EGLint attribute, EGLAttrib *value);
+EGLAPI const char *EGLAPIENTRY eglQueryOutputLayerStringEXT (EGLDisplay dpy, EGLOutputLayerEXT layer, EGLint name);
+EGLAPI EGLBoolean EGLAPIENTRY eglOutputPortAttribEXT (EGLDisplay dpy, EGLOutputPortEXT port, EGLint attribute, EGLAttrib value);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryOutputPortAttribEXT (EGLDisplay dpy, EGLOutputPortEXT port, EGLint attribute, EGLAttrib *value);
+EGLAPI const char *EGLAPIENTRY eglQueryOutputPortStringEXT (EGLDisplay dpy, EGLOutputPortEXT port, EGLint name);
+#endif
+#endif /* EGL_EXT_output_base */
+
+#ifndef EGL_EXT_output_drm
+#define EGL_EXT_output_drm 1
+#define EGL_DRM_CRTC_EXT 0x3234
+#define EGL_DRM_PLANE_EXT 0x3235
+#define EGL_DRM_CONNECTOR_EXT 0x3236
+#endif /* EGL_EXT_output_drm */
+
+#ifndef EGL_EXT_output_openwf
+#define EGL_EXT_output_openwf 1
+#define EGL_OPENWF_PIPELINE_ID_EXT 0x3238
+#define EGL_OPENWF_PORT_ID_EXT 0x3239
+#endif /* EGL_EXT_output_openwf */
+
+#ifndef EGL_EXT_pixel_format_float
+#define EGL_EXT_pixel_format_float 1
+#define EGL_COLOR_COMPONENT_TYPE_EXT 0x3339
+#define EGL_COLOR_COMPONENT_TYPE_FIXED_EXT 0x333A
+#define EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT 0x333B
+#endif /* EGL_EXT_pixel_format_float */
+
+#ifndef EGL_EXT_platform_base
+#define EGL_EXT_platform_base 1
+typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC) (EGLDisplay dpy, EGLConfig config, void *native_window, const EGLint *attrib_list);
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPLATFORMPIXMAPSURFACEEXTPROC) (EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLint *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLDisplay EGLAPIENTRY eglGetPlatformDisplayEXT (EGLenum platform, void *native_display, const EGLint *attrib_list);
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePlatformWindowSurfaceEXT (EGLDisplay dpy, EGLConfig config, void *native_window, const EGLint *attrib_list);
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePlatformPixmapSurfaceEXT (EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLint *attrib_list);
+#endif
+#endif /* EGL_EXT_platform_base */
+
+#ifndef EGL_EXT_platform_device
+#define EGL_EXT_platform_device 1
+#define EGL_PLATFORM_DEVICE_EXT 0x313F
+#endif /* EGL_EXT_platform_device */
+
+#ifndef EGL_EXT_platform_wayland
+#define EGL_EXT_platform_wayland 1
+#define EGL_PLATFORM_WAYLAND_EXT 0x31D8
+#endif /* EGL_EXT_platform_wayland */
+
+#ifndef EGL_EXT_platform_x11
+#define EGL_EXT_platform_x11 1
+#define EGL_PLATFORM_X11_EXT 0x31D5
+#define EGL_PLATFORM_X11_SCREEN_EXT 0x31D6
+#endif /* EGL_EXT_platform_x11 */
+
+#ifndef EGL_EXT_platform_xcb
+#define EGL_EXT_platform_xcb 1
+#define EGL_PLATFORM_XCB_EXT 0x31DC
+#define EGL_PLATFORM_XCB_SCREEN_EXT 0x31DE
+#endif /* EGL_EXT_platform_xcb */
+
+#ifndef EGL_EXT_present_opaque
+#define EGL_EXT_present_opaque 1
+#define EGL_PRESENT_OPAQUE_EXT 0x31DF
+#endif /* EGL_EXT_present_opaque */
+
+#ifndef EGL_EXT_protected_content
+#define EGL_EXT_protected_content 1
+#define EGL_PROTECTED_CONTENT_EXT 0x32C0
+#endif /* EGL_EXT_protected_content */
+
+#ifndef EGL_EXT_protected_surface
+#define EGL_EXT_protected_surface 1
+#endif /* EGL_EXT_protected_surface */
+
+#ifndef EGL_EXT_stream_consumer_egloutput
+#define EGL_EXT_stream_consumer_egloutput 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMCONSUMEROUTPUTEXTPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLOutputLayerEXT layer);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamConsumerOutputEXT (EGLDisplay dpy, EGLStreamKHR stream, EGLOutputLayerEXT layer);
+#endif
+#endif /* EGL_EXT_stream_consumer_egloutput */
+
+#ifndef EGL_EXT_surface_CTA861_3_metadata
+#define EGL_EXT_surface_CTA861_3_metadata 1
+#define EGL_CTA861_3_MAX_CONTENT_LIGHT_LEVEL_EXT 0x3360
+#define EGL_CTA861_3_MAX_FRAME_AVERAGE_LEVEL_EXT 0x3361
+#endif /* EGL_EXT_surface_CTA861_3_metadata */
+
+#ifndef EGL_EXT_surface_SMPTE2086_metadata
+#define EGL_EXT_surface_SMPTE2086_metadata 1
+#define EGL_SMPTE2086_DISPLAY_PRIMARY_RX_EXT 0x3341
+#define EGL_SMPTE2086_DISPLAY_PRIMARY_RY_EXT 0x3342
+#define EGL_SMPTE2086_DISPLAY_PRIMARY_GX_EXT 0x3343
+#define EGL_SMPTE2086_DISPLAY_PRIMARY_GY_EXT 0x3344
+#define EGL_SMPTE2086_DISPLAY_PRIMARY_BX_EXT 0x3345
+#define EGL_SMPTE2086_DISPLAY_PRIMARY_BY_EXT 0x3346
+#define EGL_SMPTE2086_WHITE_POINT_X_EXT 0x3347
+#define EGL_SMPTE2086_WHITE_POINT_Y_EXT 0x3348
+#define EGL_SMPTE2086_MAX_LUMINANCE_EXT 0x3349
+#define EGL_SMPTE2086_MIN_LUMINANCE_EXT 0x334A
+#define EGL_METADATA_SCALING_EXT 50000
+#endif /* EGL_EXT_surface_SMPTE2086_metadata */
+
+#ifndef EGL_EXT_surface_compression
+#define EGL_EXT_surface_compression 1
+#define EGL_SURFACE_COMPRESSION_EXT 0x34B0
+#define EGL_SURFACE_COMPRESSION_PLANE1_EXT 0x328E
+#define EGL_SURFACE_COMPRESSION_PLANE2_EXT 0x328F
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_NONE_EXT 0x34B1
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_DEFAULT_EXT 0x34B2
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_1BPC_EXT 0x34B4
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_2BPC_EXT 0x34B5
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_3BPC_EXT 0x34B6
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_4BPC_EXT 0x34B7
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_5BPC_EXT 0x34B8
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_6BPC_EXT 0x34B9
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_7BPC_EXT 0x34BA
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_8BPC_EXT 0x34BB
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_9BPC_EXT 0x34BC
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_10BPC_EXT 0x34BD
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_11BPC_EXT 0x34BE
+#define EGL_SURFACE_COMPRESSION_FIXED_RATE_12BPC_EXT 0x34BF
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSUPPORTEDCOMPRESSIONRATESEXTPROC) (EGLDisplay dpy, EGLConfig config, const EGLAttrib *attrib_list, EGLint *rates, EGLint rate_size, EGLint *num_rates);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQuerySupportedCompressionRatesEXT (EGLDisplay dpy, EGLConfig config, const EGLAttrib *attrib_list, EGLint *rates, EGLint rate_size, EGLint *num_rates);
+#endif
+#endif /* EGL_EXT_surface_compression */
+
+#ifndef EGL_EXT_swap_buffers_with_damage
+#define EGL_EXT_swap_buffers_with_damage 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC) (EGLDisplay dpy, EGLSurface surface, const EGLint *rects, EGLint n_rects);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglSwapBuffersWithDamageEXT (EGLDisplay dpy, EGLSurface surface, const EGLint *rects, EGLint n_rects);
+#endif
+#endif /* EGL_EXT_swap_buffers_with_damage */
+
+#ifndef EGL_EXT_sync_reuse
+#define EGL_EXT_sync_reuse 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLUNSIGNALSYNCEXTPROC) (EGLDisplay dpy, EGLSync sync, const EGLAttrib *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglUnsignalSyncEXT (EGLDisplay dpy, EGLSync sync, const EGLAttrib *attrib_list);
+#endif
+#endif /* EGL_EXT_sync_reuse */
+
+#ifndef EGL_EXT_yuv_surface
+#define EGL_EXT_yuv_surface 1
+#define EGL_YUV_ORDER_EXT 0x3301
+#define EGL_YUV_NUMBER_OF_PLANES_EXT 0x3311
+#define EGL_YUV_SUBSAMPLE_EXT 0x3312
+#define EGL_YUV_DEPTH_RANGE_EXT 0x3317
+#define EGL_YUV_CSC_STANDARD_EXT 0x330A
+#define EGL_YUV_PLANE_BPP_EXT 0x331A
+#define EGL_YUV_BUFFER_EXT 0x3300
+#define EGL_YUV_ORDER_YUV_EXT 0x3302
+#define EGL_YUV_ORDER_YVU_EXT 0x3303
+#define EGL_YUV_ORDER_YUYV_EXT 0x3304
+#define EGL_YUV_ORDER_UYVY_EXT 0x3305
+#define EGL_YUV_ORDER_YVYU_EXT 0x3306
+#define EGL_YUV_ORDER_VYUY_EXT 0x3307
+#define EGL_YUV_ORDER_AYUV_EXT 0x3308
+#define EGL_YUV_SUBSAMPLE_4_2_0_EXT 0x3313
+#define EGL_YUV_SUBSAMPLE_4_2_2_EXT 0x3314
+#define EGL_YUV_SUBSAMPLE_4_4_4_EXT 0x3315
+#define EGL_YUV_DEPTH_RANGE_LIMITED_EXT 0x3318
+#define EGL_YUV_DEPTH_RANGE_FULL_EXT 0x3319
+#define EGL_YUV_CSC_STANDARD_601_EXT 0x330B
+#define EGL_YUV_CSC_STANDARD_709_EXT 0x330C
+#define EGL_YUV_CSC_STANDARD_2020_EXT 0x330D
+#define EGL_YUV_PLANE_BPP_0_EXT 0x331B
+#define EGL_YUV_PLANE_BPP_8_EXT 0x331C
+#define EGL_YUV_PLANE_BPP_10_EXT 0x331D
+#endif /* EGL_EXT_yuv_surface */
+
+#ifndef EGL_HI_clientpixmap
+#define EGL_HI_clientpixmap 1
+struct EGLClientPixmapHI {
+ void *pData;
+ EGLint iWidth;
+ EGLint iHeight;
+ EGLint iStride;
+};
+#define EGL_CLIENT_PIXMAP_POINTER_HI 0x8F74
+typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPIXMAPSURFACEHIPROC) (EGLDisplay dpy, EGLConfig config, struct EGLClientPixmapHI *pixmap);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLSurface EGLAPIENTRY eglCreatePixmapSurfaceHI (EGLDisplay dpy, EGLConfig config, struct EGLClientPixmapHI *pixmap);
+#endif
+#endif /* EGL_HI_clientpixmap */
+
+#ifndef EGL_HI_colorformats
+#define EGL_HI_colorformats 1
+#define EGL_COLOR_FORMAT_HI 0x8F70
+#define EGL_COLOR_RGB_HI 0x8F71
+#define EGL_COLOR_RGBA_HI 0x8F72
+#define EGL_COLOR_ARGB_HI 0x8F73
+#endif /* EGL_HI_colorformats */
+
+#ifndef EGL_IMG_context_priority
+#define EGL_IMG_context_priority 1
+#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
+#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
+#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
+#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
+#endif /* EGL_IMG_context_priority */
+
+#ifndef EGL_IMG_image_plane_attribs
+#define EGL_IMG_image_plane_attribs 1
+#define EGL_NATIVE_BUFFER_MULTIPLANE_SEPARATE_IMG 0x3105
+#define EGL_NATIVE_BUFFER_PLANE_OFFSET_IMG 0x3106
+#endif /* EGL_IMG_image_plane_attribs */
+
+#ifndef EGL_MESA_drm_image
+#define EGL_MESA_drm_image 1
+#define EGL_DRM_BUFFER_FORMAT_MESA 0x31D0
+#define EGL_DRM_BUFFER_USE_MESA 0x31D1
+#define EGL_DRM_BUFFER_FORMAT_ARGB32_MESA 0x31D2
+#define EGL_DRM_BUFFER_MESA 0x31D3
+#define EGL_DRM_BUFFER_STRIDE_MESA 0x31D4
+#define EGL_DRM_BUFFER_USE_SCANOUT_MESA 0x00000001
+#define EGL_DRM_BUFFER_USE_SHARE_MESA 0x00000002
+#define EGL_DRM_BUFFER_USE_CURSOR_MESA 0x00000004
+typedef EGLImageKHR (EGLAPIENTRYP PFNEGLCREATEDRMIMAGEMESAPROC) (EGLDisplay dpy, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLEXPORTDRMIMAGEMESAPROC) (EGLDisplay dpy, EGLImageKHR image, EGLint *name, EGLint *handle, EGLint *stride);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLImageKHR EGLAPIENTRY eglCreateDRMImageMESA (EGLDisplay dpy, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglExportDRMImageMESA (EGLDisplay dpy, EGLImageKHR image, EGLint *name, EGLint *handle, EGLint *stride);
+#endif
+#endif /* EGL_MESA_drm_image */
+
+#ifndef EGL_MESA_image_dma_buf_export
+#define EGL_MESA_image_dma_buf_export 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLEXPORTDMABUFIMAGEQUERYMESAPROC) (EGLDisplay dpy, EGLImageKHR image, int *fourcc, int *num_planes, EGLuint64KHR *modifiers);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLEXPORTDMABUFIMAGEMESAPROC) (EGLDisplay dpy, EGLImageKHR image, int *fds, EGLint *strides, EGLint *offsets);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglExportDMABUFImageQueryMESA (EGLDisplay dpy, EGLImageKHR image, int *fourcc, int *num_planes, EGLuint64KHR *modifiers);
+EGLAPI EGLBoolean EGLAPIENTRY eglExportDMABUFImageMESA (EGLDisplay dpy, EGLImageKHR image, int *fds, EGLint *strides, EGLint *offsets);
+#endif
+#endif /* EGL_MESA_image_dma_buf_export */
+
+#ifndef EGL_MESA_platform_gbm
+#define EGL_MESA_platform_gbm 1
+#define EGL_PLATFORM_GBM_MESA 0x31D7
+#endif /* EGL_MESA_platform_gbm */
+
+#ifndef EGL_MESA_platform_surfaceless
+#define EGL_MESA_platform_surfaceless 1
+#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
+#endif /* EGL_MESA_platform_surfaceless */
+
+#ifndef EGL_MESA_query_driver
+#define EGL_MESA_query_driver 1
+typedef char *(EGLAPIENTRYP PFNEGLGETDISPLAYDRIVERCONFIGPROC) (EGLDisplay dpy);
+typedef const char *(EGLAPIENTRYP PFNEGLGETDISPLAYDRIVERNAMEPROC) (EGLDisplay dpy);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI char *EGLAPIENTRY eglGetDisplayDriverConfig (EGLDisplay dpy);
+EGLAPI const char *EGLAPIENTRY eglGetDisplayDriverName (EGLDisplay dpy);
+#endif
+#endif /* EGL_MESA_query_driver */
+
+#ifndef EGL_NOK_swap_region
+#define EGL_NOK_swap_region 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPBUFFERSREGIONNOKPROC) (EGLDisplay dpy, EGLSurface surface, EGLint numRects, const EGLint *rects);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglSwapBuffersRegionNOK (EGLDisplay dpy, EGLSurface surface, EGLint numRects, const EGLint *rects);
+#endif
+#endif /* EGL_NOK_swap_region */
+
+#ifndef EGL_NOK_swap_region2
+#define EGL_NOK_swap_region2 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSWAPBUFFERSREGION2NOKPROC) (EGLDisplay dpy, EGLSurface surface, EGLint numRects, const EGLint *rects);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglSwapBuffersRegion2NOK (EGLDisplay dpy, EGLSurface surface, EGLint numRects, const EGLint *rects);
+#endif
+#endif /* EGL_NOK_swap_region2 */
+
+#ifndef EGL_NOK_texture_from_pixmap
+#define EGL_NOK_texture_from_pixmap 1
+#define EGL_Y_INVERTED_NOK 0x307F
+#endif /* EGL_NOK_texture_from_pixmap */
+
+#ifndef EGL_NV_3dvision_surface
+#define EGL_NV_3dvision_surface 1
+#define EGL_AUTO_STEREO_NV 0x3136
+#endif /* EGL_NV_3dvision_surface */
+
+#ifndef EGL_NV_context_priority_realtime
+#define EGL_NV_context_priority_realtime 1
+#define EGL_CONTEXT_PRIORITY_REALTIME_NV 0x3357
+#endif /* EGL_NV_context_priority_realtime */
+
+#ifndef EGL_NV_coverage_sample
+#define EGL_NV_coverage_sample 1
+#define EGL_COVERAGE_BUFFERS_NV 0x30E0
+#define EGL_COVERAGE_SAMPLES_NV 0x30E1
+#endif /* EGL_NV_coverage_sample */
+
+#ifndef EGL_NV_coverage_sample_resolve
+#define EGL_NV_coverage_sample_resolve 1
+#define EGL_COVERAGE_SAMPLE_RESOLVE_NV 0x3131
+#define EGL_COVERAGE_SAMPLE_RESOLVE_DEFAULT_NV 0x3132
+#define EGL_COVERAGE_SAMPLE_RESOLVE_NONE_NV 0x3133
+#endif /* EGL_NV_coverage_sample_resolve */
+
+#ifndef EGL_NV_cuda_event
+#define EGL_NV_cuda_event 1
+#define EGL_CUDA_EVENT_HANDLE_NV 0x323B
+#define EGL_SYNC_CUDA_EVENT_NV 0x323C
+#define EGL_SYNC_CUDA_EVENT_COMPLETE_NV 0x323D
+#endif /* EGL_NV_cuda_event */
+
+#ifndef EGL_NV_depth_nonlinear
+#define EGL_NV_depth_nonlinear 1
+#define EGL_DEPTH_ENCODING_NV 0x30E2
+#define EGL_DEPTH_ENCODING_NONE_NV 0
+#define EGL_DEPTH_ENCODING_NONLINEAR_NV 0x30E3
+#endif /* EGL_NV_depth_nonlinear */
+
+#ifndef EGL_NV_device_cuda
+#define EGL_NV_device_cuda 1
+#define EGL_CUDA_DEVICE_NV 0x323A
+#endif /* EGL_NV_device_cuda */
+
+#ifndef EGL_NV_native_query
+#define EGL_NV_native_query 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYNATIVEDISPLAYNVPROC) (EGLDisplay dpy, EGLNativeDisplayType *display_id);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYNATIVEWINDOWNVPROC) (EGLDisplay dpy, EGLSurface surf, EGLNativeWindowType *window);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYNATIVEPIXMAPNVPROC) (EGLDisplay dpy, EGLSurface surf, EGLNativePixmapType *pixmap);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryNativeDisplayNV (EGLDisplay dpy, EGLNativeDisplayType *display_id);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryNativeWindowNV (EGLDisplay dpy, EGLSurface surf, EGLNativeWindowType *window);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryNativePixmapNV (EGLDisplay dpy, EGLSurface surf, EGLNativePixmapType *pixmap);
+#endif
+#endif /* EGL_NV_native_query */
+
+#ifndef EGL_NV_post_convert_rounding
+#define EGL_NV_post_convert_rounding 1
+#endif /* EGL_NV_post_convert_rounding */
+
+#ifndef EGL_NV_post_sub_buffer
+#define EGL_NV_post_sub_buffer 1
+#define EGL_POST_SUB_BUFFER_SUPPORTED_NV 0x30BE
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLPOSTSUBBUFFERNVPROC) (EGLDisplay dpy, EGLSurface surface, EGLint x, EGLint y, EGLint width, EGLint height);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglPostSubBufferNV (EGLDisplay dpy, EGLSurface surface, EGLint x, EGLint y, EGLint width, EGLint height);
+#endif
+#endif /* EGL_NV_post_sub_buffer */
+
+#ifndef EGL_NV_quadruple_buffer
+#define EGL_NV_quadruple_buffer 1
+#define EGL_QUADRUPLE_BUFFER_NV 0x3231
+#endif /* EGL_NV_quadruple_buffer */
+
+#ifndef EGL_NV_robustness_video_memory_purge
+#define EGL_NV_robustness_video_memory_purge 1
+#define EGL_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV 0x334C
+#endif /* EGL_NV_robustness_video_memory_purge */
+
+#ifndef EGL_NV_stream_consumer_eglimage
+#define EGL_NV_stream_consumer_eglimage 1
+#define EGL_STREAM_CONSUMER_IMAGE_NV 0x3373
+#define EGL_STREAM_IMAGE_ADD_NV 0x3374
+#define EGL_STREAM_IMAGE_REMOVE_NV 0x3375
+#define EGL_STREAM_IMAGE_AVAILABLE_NV 0x3376
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMIMAGECONSUMERCONNECTNVPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLint num_modifiers, const EGLuint64KHR *modifiers, const EGLAttrib *attrib_list);
+typedef EGLint (EGLAPIENTRYP PFNEGLQUERYSTREAMCONSUMEREVENTNVPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLTime timeout, EGLenum *event, EGLAttrib *aux);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMACQUIREIMAGENVPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLImage *pImage, EGLSync sync);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMRELEASEIMAGENVPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLImage image, EGLSync sync);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamImageConsumerConnectNV (EGLDisplay dpy, EGLStreamKHR stream, EGLint num_modifiers, const EGLuint64KHR *modifiers, const EGLAttrib *attrib_list);
+EGLAPI EGLint EGLAPIENTRY eglQueryStreamConsumerEventNV (EGLDisplay dpy, EGLStreamKHR stream, EGLTime timeout, EGLenum *event, EGLAttrib *aux);
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamAcquireImageNV (EGLDisplay dpy, EGLStreamKHR stream, EGLImage *pImage, EGLSync sync);
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamReleaseImageNV (EGLDisplay dpy, EGLStreamKHR stream, EGLImage image, EGLSync sync);
+#endif
+#endif /* EGL_NV_stream_consumer_eglimage */
+
+#ifndef EGL_NV_stream_consumer_gltexture_yuv
+#define EGL_NV_stream_consumer_gltexture_yuv 1
+#define EGL_YUV_PLANE0_TEXTURE_UNIT_NV 0x332C
+#define EGL_YUV_PLANE1_TEXTURE_UNIT_NV 0x332D
+#define EGL_YUV_PLANE2_TEXTURE_UNIT_NV 0x332E
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMCONSUMERGLTEXTUREEXTERNALATTRIBSNVPROC) (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamConsumerGLTextureExternalAttribsNV (EGLDisplay dpy, EGLStreamKHR stream, const EGLAttrib *attrib_list);
+#endif
+#endif /* EGL_NV_stream_consumer_gltexture_yuv */
+
+#ifndef EGL_NV_stream_cross_display
+#define EGL_NV_stream_cross_display 1
+#define EGL_STREAM_CROSS_DISPLAY_NV 0x334E
+#endif /* EGL_NV_stream_cross_display */
+
+#ifndef EGL_NV_stream_cross_object
+#define EGL_NV_stream_cross_object 1
+#define EGL_STREAM_CROSS_OBJECT_NV 0x334D
+#endif /* EGL_NV_stream_cross_object */
+
+#ifndef EGL_NV_stream_cross_partition
+#define EGL_NV_stream_cross_partition 1
+#define EGL_STREAM_CROSS_PARTITION_NV 0x323F
+#endif /* EGL_NV_stream_cross_partition */
+
+#ifndef EGL_NV_stream_cross_process
+#define EGL_NV_stream_cross_process 1
+#define EGL_STREAM_CROSS_PROCESS_NV 0x3245
+#endif /* EGL_NV_stream_cross_process */
+
+#ifndef EGL_NV_stream_cross_system
+#define EGL_NV_stream_cross_system 1
+#define EGL_STREAM_CROSS_SYSTEM_NV 0x334F
+#endif /* EGL_NV_stream_cross_system */
+
+#ifndef EGL_NV_stream_dma
+#define EGL_NV_stream_dma 1
+#define EGL_STREAM_DMA_NV 0x3371
+#define EGL_STREAM_DMA_SERVER_NV 0x3372
+#endif /* EGL_NV_stream_dma */
+
+#ifndef EGL_NV_stream_fifo_next
+#define EGL_NV_stream_fifo_next 1
+#define EGL_PENDING_FRAME_NV 0x3329
+#define EGL_STREAM_TIME_PENDING_NV 0x332A
+#endif /* EGL_NV_stream_fifo_next */
+
+#ifndef EGL_NV_stream_fifo_synchronous
+#define EGL_NV_stream_fifo_synchronous 1
+#define EGL_STREAM_FIFO_SYNCHRONOUS_NV 0x3336
+#endif /* EGL_NV_stream_fifo_synchronous */
+
+#ifndef EGL_NV_stream_flush
+#define EGL_NV_stream_flush 1
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSTREAMFLUSHNVPROC) (EGLDisplay dpy, EGLStreamKHR stream);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglStreamFlushNV (EGLDisplay dpy, EGLStreamKHR stream);
+#endif
+#endif /* EGL_NV_stream_flush */
+
+#ifndef EGL_NV_stream_frame_limits
+#define EGL_NV_stream_frame_limits 1
+#define EGL_PRODUCER_MAX_FRAME_HINT_NV 0x3337
+#define EGL_CONSUMER_MAX_FRAME_HINT_NV 0x3338
+#endif /* EGL_NV_stream_frame_limits */
+
+#ifndef EGL_NV_stream_metadata
+#define EGL_NV_stream_metadata 1
+#define EGL_MAX_STREAM_METADATA_BLOCKS_NV 0x3250
+#define EGL_MAX_STREAM_METADATA_BLOCK_SIZE_NV 0x3251
+#define EGL_MAX_STREAM_METADATA_TOTAL_SIZE_NV 0x3252
+#define EGL_PRODUCER_METADATA_NV 0x3253
+#define EGL_CONSUMER_METADATA_NV 0x3254
+#define EGL_PENDING_METADATA_NV 0x3328
+#define EGL_METADATA0_SIZE_NV 0x3255
+#define EGL_METADATA1_SIZE_NV 0x3256
+#define EGL_METADATA2_SIZE_NV 0x3257
+#define EGL_METADATA3_SIZE_NV 0x3258
+#define EGL_METADATA0_TYPE_NV 0x3259
+#define EGL_METADATA1_TYPE_NV 0x325A
+#define EGL_METADATA2_TYPE_NV 0x325B
+#define EGL_METADATA3_TYPE_NV 0x325C
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYDISPLAYATTRIBNVPROC) (EGLDisplay dpy, EGLint attribute, EGLAttrib *value);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSETSTREAMMETADATANVPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLint n, EGLint offset, EGLint size, const void *data);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYSTREAMMETADATANVPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum name, EGLint n, EGLint offset, EGLint size, void *data);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryDisplayAttribNV (EGLDisplay dpy, EGLint attribute, EGLAttrib *value);
+EGLAPI EGLBoolean EGLAPIENTRY eglSetStreamMetadataNV (EGLDisplay dpy, EGLStreamKHR stream, EGLint n, EGLint offset, EGLint size, const void *data);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryStreamMetadataNV (EGLDisplay dpy, EGLStreamKHR stream, EGLenum name, EGLint n, EGLint offset, EGLint size, void *data);
+#endif
+#endif /* EGL_NV_stream_metadata */
+
+#ifndef EGL_NV_stream_origin
+#define EGL_NV_stream_origin 1
+#define EGL_STREAM_FRAME_ORIGIN_X_NV 0x3366
+#define EGL_STREAM_FRAME_ORIGIN_Y_NV 0x3367
+#define EGL_STREAM_FRAME_MAJOR_AXIS_NV 0x3368
+#define EGL_CONSUMER_AUTO_ORIENTATION_NV 0x3369
+#define EGL_PRODUCER_AUTO_ORIENTATION_NV 0x336A
+#define EGL_LEFT_NV 0x336B
+#define EGL_RIGHT_NV 0x336C
+#define EGL_TOP_NV 0x336D
+#define EGL_BOTTOM_NV 0x336E
+#define EGL_X_AXIS_NV 0x336F
+#define EGL_Y_AXIS_NV 0x3370
+#endif /* EGL_NV_stream_origin */
+
+#ifndef EGL_NV_stream_remote
+#define EGL_NV_stream_remote 1
+#define EGL_STREAM_STATE_INITIALIZING_NV 0x3240
+#define EGL_STREAM_TYPE_NV 0x3241
+#define EGL_STREAM_PROTOCOL_NV 0x3242
+#define EGL_STREAM_ENDPOINT_NV 0x3243
+#define EGL_STREAM_LOCAL_NV 0x3244
+#define EGL_STREAM_PRODUCER_NV 0x3247
+#define EGL_STREAM_CONSUMER_NV 0x3248
+#define EGL_STREAM_PROTOCOL_FD_NV 0x3246
+#endif /* EGL_NV_stream_remote */
+
+#ifndef EGL_NV_stream_reset
+#define EGL_NV_stream_reset 1
+#define EGL_SUPPORT_RESET_NV 0x3334
+#define EGL_SUPPORT_REUSE_NV 0x3335
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLRESETSTREAMNVPROC) (EGLDisplay dpy, EGLStreamKHR stream);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglResetStreamNV (EGLDisplay dpy, EGLStreamKHR stream);
+#endif
+#endif /* EGL_NV_stream_reset */
+
+#ifndef EGL_NV_stream_socket
+#define EGL_NV_stream_socket 1
+#define EGL_STREAM_PROTOCOL_SOCKET_NV 0x324B
+#define EGL_SOCKET_HANDLE_NV 0x324C
+#define EGL_SOCKET_TYPE_NV 0x324D
+#endif /* EGL_NV_stream_socket */
+
+#ifndef EGL_NV_stream_socket_inet
+#define EGL_NV_stream_socket_inet 1
+#define EGL_SOCKET_TYPE_INET_NV 0x324F
+#endif /* EGL_NV_stream_socket_inet */
+
+#ifndef EGL_NV_stream_socket_unix
+#define EGL_NV_stream_socket_unix 1
+#define EGL_SOCKET_TYPE_UNIX_NV 0x324E
+#endif /* EGL_NV_stream_socket_unix */
+
+#ifndef EGL_NV_stream_sync
+#define EGL_NV_stream_sync 1
+#define EGL_SYNC_NEW_FRAME_NV 0x321F
+typedef EGLSyncKHR (EGLAPIENTRYP PFNEGLCREATESTREAMSYNCNVPROC) (EGLDisplay dpy, EGLStreamKHR stream, EGLenum type, const EGLint *attrib_list);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLSyncKHR EGLAPIENTRY eglCreateStreamSyncNV (EGLDisplay dpy, EGLStreamKHR stream, EGLenum type, const EGLint *attrib_list);
+#endif
+#endif /* EGL_NV_stream_sync */
+
+#ifndef EGL_NV_sync
+#define EGL_NV_sync 1
+typedef void *EGLSyncNV;
+typedef khronos_utime_nanoseconds_t EGLTimeNV;
+#ifdef KHRONOS_SUPPORT_INT64
+#define EGL_SYNC_PRIOR_COMMANDS_COMPLETE_NV 0x30E6
+#define EGL_SYNC_STATUS_NV 0x30E7
+#define EGL_SIGNALED_NV 0x30E8
+#define EGL_UNSIGNALED_NV 0x30E9
+#define EGL_SYNC_FLUSH_COMMANDS_BIT_NV 0x0001
+#define EGL_FOREVER_NV 0xFFFFFFFFFFFFFFFFull
+#define EGL_ALREADY_SIGNALED_NV 0x30EA
+#define EGL_TIMEOUT_EXPIRED_NV 0x30EB
+#define EGL_CONDITION_SATISFIED_NV 0x30EC
+#define EGL_SYNC_TYPE_NV 0x30ED
+#define EGL_SYNC_CONDITION_NV 0x30EE
+#define EGL_SYNC_FENCE_NV 0x30EF
+#define EGL_NO_SYNC_NV EGL_CAST(EGLSyncNV,0)
+typedef EGLSyncNV (EGLAPIENTRYP PFNEGLCREATEFENCESYNCNVPROC) (EGLDisplay dpy, EGLenum condition, const EGLint *attrib_list);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYSYNCNVPROC) (EGLSyncNV sync);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLFENCENVPROC) (EGLSyncNV sync);
+typedef EGLint (EGLAPIENTRYP PFNEGLCLIENTWAITSYNCNVPROC) (EGLSyncNV sync, EGLint flags, EGLTimeNV timeout);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLSIGNALSYNCNVPROC) (EGLSyncNV sync, EGLenum mode);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLGETSYNCATTRIBNVPROC) (EGLSyncNV sync, EGLint attribute, EGLint *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLSyncNV EGLAPIENTRY eglCreateFenceSyncNV (EGLDisplay dpy, EGLenum condition, const EGLint *attrib_list);
+EGLAPI EGLBoolean EGLAPIENTRY eglDestroySyncNV (EGLSyncNV sync);
+EGLAPI EGLBoolean EGLAPIENTRY eglFenceNV (EGLSyncNV sync);
+EGLAPI EGLint EGLAPIENTRY eglClientWaitSyncNV (EGLSyncNV sync, EGLint flags, EGLTimeNV timeout);
+EGLAPI EGLBoolean EGLAPIENTRY eglSignalSyncNV (EGLSyncNV sync, EGLenum mode);
+EGLAPI EGLBoolean EGLAPIENTRY eglGetSyncAttribNV (EGLSyncNV sync, EGLint attribute, EGLint *value);
+#endif
+#endif /* KHRONOS_SUPPORT_INT64 */
+#endif /* EGL_NV_sync */
+
+#ifndef EGL_NV_system_time
+#define EGL_NV_system_time 1
+typedef khronos_utime_nanoseconds_t EGLuint64NV;
+#ifdef KHRONOS_SUPPORT_INT64
+typedef EGLuint64NV (EGLAPIENTRYP PFNEGLGETSYSTEMTIMEFREQUENCYNVPROC) (void);
+typedef EGLuint64NV (EGLAPIENTRYP PFNEGLGETSYSTEMTIMENVPROC) (void);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLuint64NV EGLAPIENTRY eglGetSystemTimeFrequencyNV (void);
+EGLAPI EGLuint64NV EGLAPIENTRY eglGetSystemTimeNV (void);
+#endif
+#endif /* KHRONOS_SUPPORT_INT64 */
+#endif /* EGL_NV_system_time */
+
+#ifndef EGL_NV_triple_buffer
+#define EGL_NV_triple_buffer 1
+#define EGL_TRIPLE_BUFFER_NV 0x3230
+#endif /* EGL_NV_triple_buffer */
+
+#ifndef EGL_TIZEN_image_native_buffer
+#define EGL_TIZEN_image_native_buffer 1
+#define EGL_NATIVE_BUFFER_TIZEN 0x32A0
+#endif /* EGL_TIZEN_image_native_buffer */
+
+#ifndef EGL_TIZEN_image_native_surface
+#define EGL_TIZEN_image_native_surface 1
+#define EGL_NATIVE_SURFACE_TIZEN 0x32A1
+#endif /* EGL_TIZEN_image_native_surface */
+
+#ifndef EGL_WL_bind_wayland_display
+#define EGL_WL_bind_wayland_display 1
+#define PFNEGLBINDWAYLANDDISPLAYWL PFNEGLBINDWAYLANDDISPLAYWLPROC
+#define PFNEGLUNBINDWAYLANDDISPLAYWL PFNEGLUNBINDWAYLANDDISPLAYWLPROC
+#define PFNEGLQUERYWAYLANDBUFFERWL PFNEGLQUERYWAYLANDBUFFERWLPROC
+struct wl_display;
+struct wl_resource;
+#define EGL_WAYLAND_BUFFER_WL 0x31D5
+#define EGL_WAYLAND_PLANE_WL 0x31D6
+#define EGL_TEXTURE_Y_U_V_WL 0x31D7
+#define EGL_TEXTURE_Y_UV_WL 0x31D8
+#define EGL_TEXTURE_Y_XUXV_WL 0x31D9
+#define EGL_TEXTURE_EXTERNAL_WL 0x31DA
+#define EGL_WAYLAND_Y_INVERTED_WL 0x31DB
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLBINDWAYLANDDISPLAYWLPROC) (EGLDisplay dpy, struct wl_display *display);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLUNBINDWAYLANDDISPLAYWLPROC) (EGLDisplay dpy, struct wl_display *display);
+typedef EGLBoolean (EGLAPIENTRYP PFNEGLQUERYWAYLANDBUFFERWLPROC) (EGLDisplay dpy, struct wl_resource *buffer, EGLint attribute, EGLint *value);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI EGLBoolean EGLAPIENTRY eglBindWaylandDisplayWL (EGLDisplay dpy, struct wl_display *display);
+EGLAPI EGLBoolean EGLAPIENTRY eglUnbindWaylandDisplayWL (EGLDisplay dpy, struct wl_display *display);
+EGLAPI EGLBoolean EGLAPIENTRY eglQueryWaylandBufferWL (EGLDisplay dpy, struct wl_resource *buffer, EGLint attribute, EGLint *value);
+#endif
+#endif /* EGL_WL_bind_wayland_display */
+
+#ifndef EGL_WL_create_wayland_buffer_from_image
+#define EGL_WL_create_wayland_buffer_from_image 1
+#define PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWL PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWLPROC
+struct wl_buffer;
+typedef struct wl_buffer *(EGLAPIENTRYP PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWLPROC) (EGLDisplay dpy, EGLImageKHR image);
+#ifdef EGL_EGLEXT_PROTOTYPES
+EGLAPI struct wl_buffer *EGLAPIENTRY eglCreateWaylandBufferFromImageWL (EGLDisplay dpy, EGLImageKHR image);
+#endif
+#endif /* EGL_WL_create_wayland_buffer_from_image */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __eglext_h_ */
+
+#endif /* _MSC_VER */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_endian.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_endian.h
new file mode 100644
index 00000000..5be66eaf
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_endian.h
@@ -0,0 +1,396 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryEndian
+ *
+ * Functions for reading and writing endian-specific values
+ */
+
+#ifndef SDL_endian_h_
+#define SDL_endian_h_
+
+#include "SDL_stdinc.h"
+
+#if defined(_MSC_VER) && (_MSC_VER >= 1400)
+/* As of Clang 11, '_m_prefetchw' is conflicting with the winnt.h's version,
+ so we define the needed '_m_prefetch' here as a pseudo-header, until the issue is fixed. */
+#ifdef __clang__
+#ifndef __PRFCHWINTRIN_H
+#define __PRFCHWINTRIN_H
+static __inline__ void __attribute__((__always_inline__, __nodebug__))
+_m_prefetch(void *__P)
+{
+ __builtin_prefetch(__P, 0, 3 /* _MM_HINT_T0 */);
+}
+#endif /* __PRFCHWINTRIN_H */
+#endif /* __clang__ */
+
+#include
+#endif
+
+/**
+ * \name The two types of endianness
+ */
+/* @{ */
+#define SDL_LIL_ENDIAN 1234
+#define SDL_BIG_ENDIAN 4321
+/* @} */
+
+#ifndef SDL_BYTEORDER /* Not defined in SDL_config.h? */
+#ifdef __linux__
+#include
+#define SDL_BYTEORDER __BYTE_ORDER
+#elif defined(__sun) && defined(__SVR4) /* Solaris */
+#include
+#if defined(_LITTLE_ENDIAN)
+#define SDL_BYTEORDER SDL_LIL_ENDIAN
+#elif defined(_BIG_ENDIAN)
+#define SDL_BYTEORDER SDL_BIG_ENDIAN
+#else
+#error Unsupported endianness
+#endif
+#elif defined(__OpenBSD__) || defined(__DragonFly__)
+#include
+#define SDL_BYTEORDER BYTE_ORDER
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+#include
+#define SDL_BYTEORDER BYTE_ORDER
+/* predefs from newer gcc and clang versions: */
+#elif defined(__ORDER_LITTLE_ENDIAN__) && defined(__ORDER_BIG_ENDIAN__) && defined(__BYTE_ORDER__)
+#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+#define SDL_BYTEORDER SDL_LIL_ENDIAN
+#elif (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
+#define SDL_BYTEORDER SDL_BIG_ENDIAN
+#else
+#error Unsupported endianness
+#endif /**/
+#else
+#if defined(__hppa__) || \
+ defined(__m68k__) || defined(mc68000) || defined(_M_M68K) || \
+ (defined(__MIPS__) && defined(__MIPSEB__)) || \
+ defined(__ppc__) || defined(__POWERPC__) || defined(__powerpc__) || defined(__PPC__) || \
+ defined(__sparc__) || defined(__sparc)
+#define SDL_BYTEORDER SDL_BIG_ENDIAN
+#else
+#define SDL_BYTEORDER SDL_LIL_ENDIAN
+#endif
+#endif /* __linux__ */
+#endif /* !SDL_BYTEORDER */
+
+#ifndef SDL_FLOATWORDORDER /* Not defined in SDL_config.h? */
+/* predefs from newer gcc versions: */
+#if defined(__ORDER_LITTLE_ENDIAN__) && defined(__ORDER_BIG_ENDIAN__) && defined(__FLOAT_WORD_ORDER__)
+#if (__FLOAT_WORD_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+#define SDL_FLOATWORDORDER SDL_LIL_ENDIAN
+#elif (__FLOAT_WORD_ORDER__ == __ORDER_BIG_ENDIAN__)
+#define SDL_FLOATWORDORDER SDL_BIG_ENDIAN
+#else
+#error Unsupported endianness
+#endif /**/
+#elif defined(__MAVERICK__)
+/* For Maverick, float words are always little-endian. */
+#define SDL_FLOATWORDORDER SDL_LIL_ENDIAN
+#elif (defined(__arm__) || defined(__thumb__)) && !defined(__VFP_FP__) && !defined(__ARM_EABI__)
+/* For FPA, float words are always big-endian. */
+#define SDL_FLOATWORDORDER SDL_BIG_ENDIAN
+#else
+/* By default, assume that floats words follow the memory system mode. */
+#define SDL_FLOATWORDORDER SDL_BYTEORDER
+#endif /* __FLOAT_WORD_ORDER__ */
+#endif /* !SDL_FLOATWORDORDER */
+
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \file SDL_endian.h
+ */
+
+/* various modern compilers may have builtin swap */
+#if defined(__GNUC__) || defined(__clang__)
+# define HAS_BUILTIN_BSWAP16 (_SDL_HAS_BUILTIN(__builtin_bswap16)) || \
+ (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8))
+# define HAS_BUILTIN_BSWAP32 (_SDL_HAS_BUILTIN(__builtin_bswap32)) || \
+ (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))
+# define HAS_BUILTIN_BSWAP64 (_SDL_HAS_BUILTIN(__builtin_bswap64)) || \
+ (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))
+
+ /* this one is broken */
+# define HAS_BROKEN_BSWAP (__GNUC__ == 2 && __GNUC_MINOR__ <= 95)
+#else
+# define HAS_BUILTIN_BSWAP16 0
+# define HAS_BUILTIN_BSWAP32 0
+# define HAS_BUILTIN_BSWAP64 0
+# define HAS_BROKEN_BSWAP 0
+#endif
+
+#if HAS_BUILTIN_BSWAP16
+#define SDL_Swap16(x) __builtin_bswap16(x)
+#elif (defined(_MSC_VER) && (_MSC_VER >= 1400)) && !defined(__ICL)
+#pragma intrinsic(_byteswap_ushort)
+#define SDL_Swap16(x) _byteswap_ushort(x)
+#elif defined(__i386__) && !HAS_BROKEN_BSWAP
+SDL_FORCE_INLINE Uint16
+SDL_Swap16(Uint16 x)
+{
+ __asm__("xchgb %b0,%h0": "=q"(x):"0"(x));
+ return x;
+}
+#elif defined(__x86_64__)
+SDL_FORCE_INLINE Uint16
+SDL_Swap16(Uint16 x)
+{
+ __asm__("xchgb %b0,%h0": "=Q"(x):"0"(x));
+ return x;
+}
+#elif (defined(__powerpc__) || defined(__ppc__))
+SDL_FORCE_INLINE Uint16
+SDL_Swap16(Uint16 x)
+{
+ int result;
+
+ __asm__("rlwimi %0,%2,8,16,23": "=&r"(result):"0"(x >> 8), "r"(x));
+ return (Uint16)result;
+}
+#elif (defined(__m68k__) && !defined(__mcoldfire__))
+SDL_FORCE_INLINE Uint16
+SDL_Swap16(Uint16 x)
+{
+ __asm__("rorw #8,%0": "=d"(x): "0"(x):"cc");
+ return x;
+}
+#elif defined(__WATCOMC__) && defined(__386__)
+extern __inline Uint16 SDL_Swap16(Uint16);
+#pragma aux SDL_Swap16 = \
+ "xchg al, ah" \
+ parm [ax] \
+ modify [ax];
+#else
+
+/**
+ * Use this function to swap the byte order of a 16-bit value.
+ *
+ * \param x the value to be swapped.
+ * \returns the swapped value.
+ *
+ * \sa SDL_SwapBE16
+ * \sa SDL_SwapLE16
+ */
+SDL_FORCE_INLINE Uint16
+SDL_Swap16(Uint16 x)
+{
+ return SDL_static_cast(Uint16, ((x << 8) | (x >> 8)));
+}
+#endif
+
+#if HAS_BUILTIN_BSWAP32
+#define SDL_Swap32(x) __builtin_bswap32(x)
+#elif (defined(_MSC_VER) && (_MSC_VER >= 1400)) && !defined(__ICL)
+#pragma intrinsic(_byteswap_ulong)
+#define SDL_Swap32(x) _byteswap_ulong(x)
+#elif defined(__i386__) && !HAS_BROKEN_BSWAP
+SDL_FORCE_INLINE Uint32
+SDL_Swap32(Uint32 x)
+{
+ __asm__("bswap %0": "=r"(x):"0"(x));
+ return x;
+}
+#elif defined(__x86_64__)
+SDL_FORCE_INLINE Uint32
+SDL_Swap32(Uint32 x)
+{
+ __asm__("bswapl %0": "=r"(x):"0"(x));
+ return x;
+}
+#elif (defined(__powerpc__) || defined(__ppc__))
+SDL_FORCE_INLINE Uint32
+SDL_Swap32(Uint32 x)
+{
+ Uint32 result;
+
+ __asm__("rlwimi %0,%2,24,16,23": "=&r"(result): "0" (x>>24), "r"(x));
+ __asm__("rlwimi %0,%2,8,8,15" : "=&r"(result): "0" (result), "r"(x));
+ __asm__("rlwimi %0,%2,24,0,7" : "=&r"(result): "0" (result), "r"(x));
+ return result;
+}
+#elif (defined(__m68k__) && !defined(__mcoldfire__))
+SDL_FORCE_INLINE Uint32
+SDL_Swap32(Uint32 x)
+{
+ __asm__("rorw #8,%0\n\tswap %0\n\trorw #8,%0": "=d"(x): "0"(x):"cc");
+ return x;
+}
+#elif defined(__WATCOMC__) && defined(__386__)
+extern __inline Uint32 SDL_Swap32(Uint32);
+#pragma aux SDL_Swap32 = \
+ "bswap eax" \
+ parm [eax] \
+ modify [eax];
+#else
+
+/**
+ * Use this function to swap the byte order of a 32-bit value.
+ *
+ * \param x the value to be swapped.
+ * \returns the swapped value.
+ *
+ * \sa SDL_SwapBE32
+ * \sa SDL_SwapLE32
+ */
+SDL_FORCE_INLINE Uint32
+SDL_Swap32(Uint32 x)
+{
+ return SDL_static_cast(Uint32, ((x << 24) | ((x << 8) & 0x00FF0000) |
+ ((x >> 8) & 0x0000FF00) | (x >> 24)));
+}
+#endif
+
+#if HAS_BUILTIN_BSWAP64
+#define SDL_Swap64(x) __builtin_bswap64(x)
+#elif (defined(_MSC_VER) && (_MSC_VER >= 1400)) && !defined(__ICL)
+#pragma intrinsic(_byteswap_uint64)
+#define SDL_Swap64(x) _byteswap_uint64(x)
+#elif defined(__i386__) && !HAS_BROKEN_BSWAP
+SDL_FORCE_INLINE Uint64
+SDL_Swap64(Uint64 x)
+{
+ union {
+ struct {
+ Uint32 a, b;
+ } s;
+ Uint64 u;
+ } v;
+ v.u = x;
+ __asm__("bswapl %0 ; bswapl %1 ; xchgl %0,%1"
+ : "=r"(v.s.a), "=r"(v.s.b)
+ : "0" (v.s.a), "1"(v.s.b));
+ return v.u;
+}
+#elif defined(__x86_64__)
+SDL_FORCE_INLINE Uint64
+SDL_Swap64(Uint64 x)
+{
+ __asm__("bswapq %0": "=r"(x):"0"(x));
+ return x;
+}
+#elif defined(__WATCOMC__) && defined(__386__)
+extern __inline Uint64 SDL_Swap64(Uint64);
+#pragma aux SDL_Swap64 = \
+ "bswap eax" \
+ "bswap edx" \
+ "xchg eax,edx" \
+ parm [eax edx] \
+ modify [eax edx];
+#else
+
+/**
+ * Use this function to swap the byte order of a 64-bit value.
+ *
+ * \param x the value to be swapped.
+ * \returns the swapped value.
+ *
+ * \sa SDL_SwapBE64
+ * \sa SDL_SwapLE64
+ */
+SDL_FORCE_INLINE Uint64
+SDL_Swap64(Uint64 x)
+{
+ Uint32 hi, lo;
+
+ /* Separate into high and low 32-bit values and swap them */
+ lo = SDL_static_cast(Uint32, x & 0xFFFFFFFF);
+ x >>= 32;
+ hi = SDL_static_cast(Uint32, x & 0xFFFFFFFF);
+ x = SDL_Swap32(lo);
+ x <<= 32;
+ x |= SDL_Swap32(hi);
+ return (x);
+}
+#endif
+
+
+/**
+ * Use this function to swap the byte order of a floating point value.
+ *
+ * \param x the value to be swapped.
+ * \returns the swapped value.
+ *
+ * \sa SDL_SwapFloatBE
+ * \sa SDL_SwapFloatLE
+ */
+SDL_FORCE_INLINE float
+SDL_SwapFloat(float x)
+{
+ union {
+ float f;
+ Uint32 ui32;
+ } swapper;
+ swapper.f = x;
+ swapper.ui32 = SDL_Swap32(swapper.ui32);
+ return swapper.f;
+}
+
+/* remove extra macros */
+#undef HAS_BROKEN_BSWAP
+#undef HAS_BUILTIN_BSWAP16
+#undef HAS_BUILTIN_BSWAP32
+#undef HAS_BUILTIN_BSWAP64
+
+/**
+ * \name Swap to native
+ * Byteswap item from the specified endianness to the native endianness.
+ */
+/* @{ */
+#if SDL_BYTEORDER == SDL_LIL_ENDIAN
+#define SDL_SwapLE16(X) (X)
+#define SDL_SwapLE32(X) (X)
+#define SDL_SwapLE64(X) (X)
+#define SDL_SwapFloatLE(X) (X)
+#define SDL_SwapBE16(X) SDL_Swap16(X)
+#define SDL_SwapBE32(X) SDL_Swap32(X)
+#define SDL_SwapBE64(X) SDL_Swap64(X)
+#define SDL_SwapFloatBE(X) SDL_SwapFloat(X)
+#else
+#define SDL_SwapLE16(X) SDL_Swap16(X)
+#define SDL_SwapLE32(X) SDL_Swap32(X)
+#define SDL_SwapLE64(X) SDL_Swap64(X)
+#define SDL_SwapFloatLE(X) SDL_SwapFloat(X)
+#define SDL_SwapBE16(X) (X)
+#define SDL_SwapBE32(X) (X)
+#define SDL_SwapBE64(X) (X)
+#define SDL_SwapFloatBE(X) (X)
+#endif
+/* @} *//* Swap to native */
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_endian_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_error.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_error.h
new file mode 100644
index 00000000..8d9cde0e
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_error.h
@@ -0,0 +1,163 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryError
+ *
+ * Simple error message routines for SDL.
+ */
+
+#ifndef SDL_error_h_
+#define SDL_error_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Public functions */
+
+
+/**
+ * Set the SDL error message for the current thread.
+ *
+ * Calling this function will replace any previous error message that was set.
+ *
+ * This function always returns -1, since SDL frequently uses -1 to signify an
+ * failing result, leading to this idiom:
+ *
+ * ```c
+ * if (error_code) {
+ * return SDL_SetError("This operation has failed: %d", error_code);
+ * }
+ * ```
+ *
+ * \param fmt a printf()-style message format string.
+ * \param ... additional parameters matching % tokens in the `fmt` string, if
+ * any.
+ * \returns always -1.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_ClearError
+ * \sa SDL_GetError
+ */
+extern DECLSPEC int SDLCALL SDL_SetError(SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(1);
+
+/**
+ * Retrieve a message about the last error that occurred on the current
+ * thread.
+ *
+ * It is possible for multiple errors to occur before calling SDL_GetError().
+ * Only the last error is returned.
+ *
+ * The message is only applicable when an SDL function has signaled an error.
+ * You must check the return values of SDL function calls to determine when to
+ * appropriately call SDL_GetError(). You should *not* use the results of
+ * SDL_GetError() to decide if an error has occurred! Sometimes SDL will set
+ * an error string even when reporting success.
+ *
+ * SDL will *not* clear the error string for successful API calls. You *must*
+ * check return values for failure cases before you can assume the error
+ * string applies.
+ *
+ * Error strings are set per-thread, so an error set in a different thread
+ * will not interfere with the current thread's operation.
+ *
+ * The returned string is internally allocated and must not be freed by the
+ * application.
+ *
+ * \returns a message with information about the specific error that occurred,
+ * or an empty string if there hasn't been an error message set since
+ * the last call to SDL_ClearError(). The message is only applicable
+ * when an SDL function has signaled an error. You must check the
+ * return values of SDL function calls to determine when to
+ * appropriately call SDL_GetError().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_ClearError
+ * \sa SDL_SetError
+ */
+extern DECLSPEC const char *SDLCALL SDL_GetError(void);
+
+/**
+ * Get the last error message that was set for the current thread.
+ *
+ * This allows the caller to copy the error string into a provided buffer, but
+ * otherwise operates exactly the same as SDL_GetError().
+ *
+ * \param errstr A buffer to fill with the last error message that was set for
+ * the current thread.
+ * \param maxlen The size of the buffer pointed to by the errstr parameter.
+ * \returns the pointer passed in as the `errstr` parameter.
+ *
+ * \since This function is available since SDL 2.0.14.
+ *
+ * \sa SDL_GetError
+ */
+extern DECLSPEC char * SDLCALL SDL_GetErrorMsg(char *errstr, int maxlen);
+
+/**
+ * Clear any previous error message for this thread.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetError
+ * \sa SDL_SetError
+ */
+extern DECLSPEC void SDLCALL SDL_ClearError(void);
+
+/**
+ * \name Internal error functions
+ *
+ * \internal
+ * Private error reporting function - used internally.
+ */
+/* @{ */
+#define SDL_OutOfMemory() SDL_Error(SDL_ENOMEM)
+#define SDL_Unsupported() SDL_Error(SDL_UNSUPPORTED)
+#define SDL_InvalidParamError(param) SDL_SetError("Parameter '%s' is invalid", (param))
+typedef enum
+{
+ SDL_ENOMEM,
+ SDL_EFREAD,
+ SDL_EFWRITE,
+ SDL_EFSEEK,
+ SDL_UNSUPPORTED,
+ SDL_LASTERROR
+} SDL_errorcode;
+/* SDL_Error() unconditionally returns -1. */
+extern DECLSPEC int SDLCALL SDL_Error(SDL_errorcode code);
+/* @} *//* Internal error functions */
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_error_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_events.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_events.h
new file mode 100644
index 00000000..b9596c0e
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_events.h
@@ -0,0 +1,1196 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryEvents
+ *
+ * Include file for SDL event handling.
+ */
+
+#ifndef SDL_events_h_
+#define SDL_events_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+#include "SDL_video.h"
+#include "SDL_keyboard.h"
+#include "SDL_mouse.h"
+#include "SDL_joystick.h"
+#include "SDL_gamecontroller.h"
+#include "SDL_quit.h"
+#include "SDL_gesture.h"
+#include "SDL_touch.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* General keyboard/mouse state definitions */
+#define SDL_RELEASED 0
+#define SDL_PRESSED 1
+
+/**
+ * The types of events that can be delivered.
+ */
+typedef enum SDL_EventType
+{
+ SDL_FIRSTEVENT = 0, /**< Unused (do not remove) */
+
+ /* Application events */
+ SDL_QUIT = 0x100, /**< User-requested quit */
+
+ /* These application events have special meaning on iOS, see README-ios.md for details */
+ SDL_APP_TERMINATING, /**< The application is being terminated by the OS
+ Called on iOS in applicationWillTerminate()
+ Called on Android in onDestroy()
+ */
+ SDL_APP_LOWMEMORY, /**< The application is low on memory, free memory if possible.
+ Called on iOS in applicationDidReceiveMemoryWarning()
+ Called on Android in onLowMemory()
+ */
+ SDL_APP_WILLENTERBACKGROUND, /**< The application is about to enter the background
+ Called on iOS in applicationWillResignActive()
+ Called on Android in onPause()
+ */
+ SDL_APP_DIDENTERBACKGROUND, /**< The application did enter the background and may not get CPU for some time
+ Called on iOS in applicationDidEnterBackground()
+ Called on Android in onPause()
+ */
+ SDL_APP_WILLENTERFOREGROUND, /**< The application is about to enter the foreground
+ Called on iOS in applicationWillEnterForeground()
+ Called on Android in onResume()
+ */
+ SDL_APP_DIDENTERFOREGROUND, /**< The application is now interactive
+ Called on iOS in applicationDidBecomeActive()
+ Called on Android in onResume()
+ */
+
+ SDL_LOCALECHANGED, /**< The user's locale preferences have changed. */
+
+ /* Display events */
+ SDL_DISPLAYEVENT = 0x150, /**< Display state change */
+
+ /* Window events */
+ SDL_WINDOWEVENT = 0x200, /**< Window state change */
+ SDL_SYSWMEVENT, /**< System specific event */
+
+ /* Keyboard events */
+ SDL_KEYDOWN = 0x300, /**< Key pressed */
+ SDL_KEYUP, /**< Key released */
+ SDL_TEXTEDITING, /**< Keyboard text editing (composition) */
+ SDL_TEXTINPUT, /**< Keyboard text input */
+ SDL_KEYMAPCHANGED, /**< Keymap changed due to a system event such as an
+ input language or keyboard layout change.
+ */
+ SDL_TEXTEDITING_EXT, /**< Extended keyboard text editing (composition) */
+
+ /* Mouse events */
+ SDL_MOUSEMOTION = 0x400, /**< Mouse moved */
+ SDL_MOUSEBUTTONDOWN, /**< Mouse button pressed */
+ SDL_MOUSEBUTTONUP, /**< Mouse button released */
+ SDL_MOUSEWHEEL, /**< Mouse wheel motion */
+
+ /* Joystick events */
+ SDL_JOYAXISMOTION = 0x600, /**< Joystick axis motion */
+ SDL_JOYBALLMOTION, /**< Joystick trackball motion */
+ SDL_JOYHATMOTION, /**< Joystick hat position change */
+ SDL_JOYBUTTONDOWN, /**< Joystick button pressed */
+ SDL_JOYBUTTONUP, /**< Joystick button released */
+ SDL_JOYDEVICEADDED, /**< A new joystick has been inserted into the system */
+ SDL_JOYDEVICEREMOVED, /**< An opened joystick has been removed */
+ SDL_JOYBATTERYUPDATED, /**< Joystick battery level change */
+
+ /* Game controller events */
+ SDL_CONTROLLERAXISMOTION = 0x650, /**< Game controller axis motion */
+ SDL_CONTROLLERBUTTONDOWN, /**< Game controller button pressed */
+ SDL_CONTROLLERBUTTONUP, /**< Game controller button released */
+ SDL_CONTROLLERDEVICEADDED, /**< A new Game controller has been inserted into the system */
+ SDL_CONTROLLERDEVICEREMOVED, /**< An opened Game controller has been removed */
+ SDL_CONTROLLERDEVICEREMAPPED, /**< The controller mapping was updated */
+ SDL_CONTROLLERTOUCHPADDOWN, /**< Game controller touchpad was touched */
+ SDL_CONTROLLERTOUCHPADMOTION, /**< Game controller touchpad finger was moved */
+ SDL_CONTROLLERTOUCHPADUP, /**< Game controller touchpad finger was lifted */
+ SDL_CONTROLLERSENSORUPDATE, /**< Game controller sensor was updated */
+ SDL_CONTROLLERUPDATECOMPLETE_RESERVED_FOR_SDL3,
+ SDL_CONTROLLERSTEAMHANDLEUPDATED, /**< Game controller Steam handle has changed */
+
+ /* Touch events */
+ SDL_FINGERDOWN = 0x700,
+ SDL_FINGERUP,
+ SDL_FINGERMOTION,
+
+ /* Gesture events */
+ SDL_DOLLARGESTURE = 0x800,
+ SDL_DOLLARRECORD,
+ SDL_MULTIGESTURE,
+
+ /* Clipboard events */
+ SDL_CLIPBOARDUPDATE = 0x900, /**< The clipboard or primary selection changed */
+
+ /* Drag and drop events */
+ SDL_DROPFILE = 0x1000, /**< The system requests a file open */
+ SDL_DROPTEXT, /**< text/plain drag-and-drop event */
+ SDL_DROPBEGIN, /**< A new set of drops is beginning (NULL filename) */
+ SDL_DROPCOMPLETE, /**< Current set of drops is now complete (NULL filename) */
+
+ /* Audio hotplug events */
+ SDL_AUDIODEVICEADDED = 0x1100, /**< A new audio device is available */
+ SDL_AUDIODEVICEREMOVED, /**< An audio device has been removed. */
+
+ /* Sensor events */
+ SDL_SENSORUPDATE = 0x1200, /**< A sensor was updated */
+
+ /* Render events */
+ SDL_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */
+ SDL_RENDER_DEVICE_RESET, /**< The device has been reset and all textures need to be recreated */
+
+ /* Internal events */
+ SDL_POLLSENTINEL = 0x7F00, /**< Signals the end of an event poll cycle */
+
+ /** Events SDL_USEREVENT through SDL_LASTEVENT are for your use,
+ * and should be allocated with SDL_RegisterEvents()
+ */
+ SDL_USEREVENT = 0x8000,
+
+ /**
+ * This last event is only for bounding internal arrays
+ */
+ SDL_LASTEVENT = 0xFFFF
+} SDL_EventType;
+
+/**
+ * Fields shared by every event
+ */
+typedef struct SDL_CommonEvent
+{
+ Uint32 type;
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+} SDL_CommonEvent;
+
+/**
+ * Display state change event data (event.display.*)
+ */
+typedef struct SDL_DisplayEvent
+{
+ Uint32 type; /**< SDL_DISPLAYEVENT */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 display; /**< The associated display index */
+ Uint8 event; /**< SDL_DisplayEventID */
+ Uint8 padding1;
+ Uint8 padding2;
+ Uint8 padding3;
+ Sint32 data1; /**< event dependent data */
+} SDL_DisplayEvent;
+
+/**
+ * Window state change event data (event.window.*)
+ */
+typedef struct SDL_WindowEvent
+{
+ Uint32 type; /**< SDL_WINDOWEVENT */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The associated window */
+ Uint8 event; /**< SDL_WindowEventID */
+ Uint8 padding1;
+ Uint8 padding2;
+ Uint8 padding3;
+ Sint32 data1; /**< event dependent data */
+ Sint32 data2; /**< event dependent data */
+} SDL_WindowEvent;
+
+/**
+ * Keyboard button event structure (event.key.*)
+ */
+typedef struct SDL_KeyboardEvent
+{
+ Uint32 type; /**< SDL_KEYDOWN or SDL_KEYUP */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The window with keyboard focus, if any */
+ Uint8 state; /**< SDL_PRESSED or SDL_RELEASED */
+ Uint8 repeat; /**< Non-zero if this is a key repeat */
+ Uint8 padding2;
+ Uint8 padding3;
+ SDL_Keysym keysym; /**< The key that was pressed or released */
+} SDL_KeyboardEvent;
+
+#define SDL_TEXTEDITINGEVENT_TEXT_SIZE (32)
+
+/**
+ * Keyboard text editing event structure (event.edit.*)
+ */
+typedef struct SDL_TextEditingEvent
+{
+ Uint32 type; /**< SDL_TEXTEDITING */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The window with keyboard focus, if any */
+ char text[SDL_TEXTEDITINGEVENT_TEXT_SIZE]; /**< The editing text */
+ Sint32 start; /**< The start cursor of selected editing text */
+ Sint32 length; /**< The length of selected editing text */
+} SDL_TextEditingEvent;
+
+/**
+ * Extended keyboard text editing event structure (event.editExt.*) when text
+ * would be truncated if stored in the text buffer SDL_TextEditingEvent
+ */
+typedef struct SDL_TextEditingExtEvent
+{
+ Uint32 type; /**< SDL_TEXTEDITING_EXT */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The window with keyboard focus, if any */
+ char* text; /**< The editing text, which should be freed with SDL_free(), and will not be NULL */
+ Sint32 start; /**< The start cursor of selected editing text */
+ Sint32 length; /**< The length of selected editing text */
+} SDL_TextEditingExtEvent;
+
+/**
+ * The maximum bytes of text that can be supplied in an SDL_TextInputEvent.
+ */
+#define SDL_TEXTINPUTEVENT_TEXT_SIZE (32)
+
+/**
+ * Keyboard text input event structure (event.text.*)
+ *
+ * `text` is limited to SDL_TEXTINPUTEVENT_TEXT_SIZE bytes. If the incoming
+ * string is larger than this, SDL will split it and send it in pieces, across
+ * multiple events. The string is in UTF-8 format, and if split, SDL
+ * guarantees that it will not split in the middle of a UTF-8 sequence, so any
+ * event will only contain complete codepoints. However, if there are several
+ * codepoints that go together into a single glyph (like an emoji "thumbs up"
+ * followed by a skin color), they may be split between events.
+ *
+ * This event will never be delivered unless text input is enabled by calling
+ * SDL_StartTextInput(). Text input is enabled by default on desktop
+ * platforms, and disabled by default on mobile platforms!
+ *
+ * \sa SDL_StartTextInput
+ * \sa SDL_StopTextInput
+ */
+typedef struct SDL_TextInputEvent
+{
+ Uint32 type; /**< SDL_TEXTINPUT */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The window with keyboard focus, if any */
+ char text[SDL_TEXTINPUTEVENT_TEXT_SIZE]; /**< The input text; UTF-8 encoded. */
+} SDL_TextInputEvent;
+
+/**
+ * Mouse motion event structure (event.motion.*)
+ */
+typedef struct SDL_MouseMotionEvent
+{
+ Uint32 type; /**< SDL_MOUSEMOTION */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The window with mouse focus, if any */
+ Uint32 which; /**< The mouse instance id, or SDL_TOUCH_MOUSEID */
+ Uint32 state; /**< The current button state */
+ Sint32 x; /**< X coordinate, relative to window */
+ Sint32 y; /**< Y coordinate, relative to window */
+ Sint32 xrel; /**< The relative motion in the X direction */
+ Sint32 yrel; /**< The relative motion in the Y direction */
+} SDL_MouseMotionEvent;
+
+/**
+ * Mouse button event structure (event.button.*)
+ */
+typedef struct SDL_MouseButtonEvent
+{
+ Uint32 type; /**< SDL_MOUSEBUTTONDOWN or SDL_MOUSEBUTTONUP */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The window with mouse focus, if any */
+ Uint32 which; /**< The mouse instance id, or SDL_TOUCH_MOUSEID */
+ Uint8 button; /**< The mouse button index */
+ Uint8 state; /**< SDL_PRESSED or SDL_RELEASED */
+ Uint8 clicks; /**< 1 for single-click, 2 for double-click, etc. */
+ Uint8 padding1;
+ Sint32 x; /**< X coordinate, relative to window */
+ Sint32 y; /**< Y coordinate, relative to window */
+} SDL_MouseButtonEvent;
+
+/**
+ * Mouse wheel event structure (event.wheel.*)
+ */
+typedef struct SDL_MouseWheelEvent
+{
+ Uint32 type; /**< SDL_MOUSEWHEEL */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The window with mouse focus, if any */
+ Uint32 which; /**< The mouse instance id, or SDL_TOUCH_MOUSEID */
+ Sint32 x; /**< The amount scrolled horizontally, positive to the right and negative to the left */
+ Sint32 y; /**< The amount scrolled vertically, positive away from the user and negative toward the user */
+ Uint32 direction; /**< Set to one of the SDL_MOUSEWHEEL_* defines. When FLIPPED the values in X and Y will be opposite. Multiply by -1 to change them back */
+ float preciseX; /**< The amount scrolled horizontally, positive to the right and negative to the left, with float precision (added in 2.0.18) */
+ float preciseY; /**< The amount scrolled vertically, positive away from the user and negative toward the user, with float precision (added in 2.0.18) */
+ Sint32 mouseX; /**< X coordinate, relative to window (added in 2.26.0) */
+ Sint32 mouseY; /**< Y coordinate, relative to window (added in 2.26.0) */
+} SDL_MouseWheelEvent;
+
+/**
+ * Joystick axis motion event structure (event.jaxis.*)
+ */
+typedef struct SDL_JoyAxisEvent
+{
+ Uint32 type; /**< SDL_JOYAXISMOTION */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Uint8 axis; /**< The joystick axis index */
+ Uint8 padding1;
+ Uint8 padding2;
+ Uint8 padding3;
+ Sint16 value; /**< The axis value (range: -32768 to 32767) */
+ Uint16 padding4;
+} SDL_JoyAxisEvent;
+
+/**
+ * Joystick trackball motion event structure (event.jball.*)
+ */
+typedef struct SDL_JoyBallEvent
+{
+ Uint32 type; /**< SDL_JOYBALLMOTION */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Uint8 ball; /**< The joystick trackball index */
+ Uint8 padding1;
+ Uint8 padding2;
+ Uint8 padding3;
+ Sint16 xrel; /**< The relative motion in the X direction */
+ Sint16 yrel; /**< The relative motion in the Y direction */
+} SDL_JoyBallEvent;
+
+/**
+ * Joystick hat position change event structure (event.jhat.*)
+ */
+typedef struct SDL_JoyHatEvent
+{
+ Uint32 type; /**< SDL_JOYHATMOTION */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Uint8 hat; /**< The joystick hat index */
+ Uint8 value; /**< The hat position value.
+ * \sa SDL_HAT_LEFTUP SDL_HAT_UP SDL_HAT_RIGHTUP
+ * \sa SDL_HAT_LEFT SDL_HAT_CENTERED SDL_HAT_RIGHT
+ * \sa SDL_HAT_LEFTDOWN SDL_HAT_DOWN SDL_HAT_RIGHTDOWN
+ *
+ * Note that zero means the POV is centered.
+ */
+ Uint8 padding1;
+ Uint8 padding2;
+} SDL_JoyHatEvent;
+
+/**
+ * Joystick button event structure (event.jbutton.*)
+ */
+typedef struct SDL_JoyButtonEvent
+{
+ Uint32 type; /**< SDL_JOYBUTTONDOWN or SDL_JOYBUTTONUP */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Uint8 button; /**< The joystick button index */
+ Uint8 state; /**< SDL_PRESSED or SDL_RELEASED */
+ Uint8 padding1;
+ Uint8 padding2;
+} SDL_JoyButtonEvent;
+
+/**
+ * Joystick device event structure (event.jdevice.*)
+ *
+ * SDL will send JOYSTICK_ADDED events for devices that are already plugged in
+ * during SDL_Init.
+ *
+ * \sa SDL_ControllerDeviceEvent
+ */
+typedef struct SDL_JoyDeviceEvent
+{
+ Uint32 type; /**< SDL_JOYDEVICEADDED or SDL_JOYDEVICEREMOVED */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Sint32 which; /**< The joystick device index for the ADDED event, instance id for the REMOVED event */
+} SDL_JoyDeviceEvent;
+
+/**
+ * Joysick battery level change event structure (event.jbattery.*)
+ */
+typedef struct SDL_JoyBatteryEvent
+{
+ Uint32 type; /**< SDL_JOYBATTERYUPDATED */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ SDL_JoystickPowerLevel level; /**< The joystick battery level */
+} SDL_JoyBatteryEvent;
+
+/**
+ * Game controller axis motion event structure (event.caxis.*)
+ */
+typedef struct SDL_ControllerAxisEvent
+{
+ Uint32 type; /**< SDL_CONTROLLERAXISMOTION */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Uint8 axis; /**< The controller axis (SDL_GameControllerAxis) */
+ Uint8 padding1;
+ Uint8 padding2;
+ Uint8 padding3;
+ Sint16 value; /**< The axis value (range: -32768 to 32767) */
+ Uint16 padding4;
+} SDL_ControllerAxisEvent;
+
+
+/**
+ * Game controller button event structure (event.cbutton.*)
+ */
+typedef struct SDL_ControllerButtonEvent
+{
+ Uint32 type; /**< SDL_CONTROLLERBUTTONDOWN or SDL_CONTROLLERBUTTONUP */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Uint8 button; /**< The controller button (SDL_GameControllerButton) */
+ Uint8 state; /**< SDL_PRESSED or SDL_RELEASED */
+ Uint8 padding1;
+ Uint8 padding2;
+} SDL_ControllerButtonEvent;
+
+
+/**
+ * Controller device event structure (event.cdevice.*)
+ *
+ * Joysticks that are supported game controllers receive both an
+ * SDL_JoyDeviceEvent and an SDL_ControllerDeviceEvent.
+ *
+ * SDL will send CONTROLLERDEVICEADDED events for joysticks that are already
+ * plugged in during SDL_Init() and are recognized as game controllers.
+ */
+typedef struct SDL_ControllerDeviceEvent
+{
+ Uint32 type; /**< SDL_CONTROLLERDEVICEADDED, SDL_CONTROLLERDEVICEREMOVED, SDL_CONTROLLERDEVICEREMAPPED, or SDL_CONTROLLERSTEAMHANDLEUPDATED */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Sint32 which; /**< The joystick device index for the ADDED event, instance id for the REMOVED or REMAPPED event */
+} SDL_ControllerDeviceEvent;
+
+/**
+ * Game controller touchpad event structure (event.ctouchpad.*)
+ */
+typedef struct SDL_ControllerTouchpadEvent
+{
+ Uint32 type; /**< SDL_CONTROLLERTOUCHPADDOWN or SDL_CONTROLLERTOUCHPADMOTION or SDL_CONTROLLERTOUCHPADUP */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Sint32 touchpad; /**< The index of the touchpad */
+ Sint32 finger; /**< The index of the finger on the touchpad */
+ float x; /**< Normalized in the range 0...1 with 0 being on the left */
+ float y; /**< Normalized in the range 0...1 with 0 being at the top */
+ float pressure; /**< Normalized in the range 0...1 */
+} SDL_ControllerTouchpadEvent;
+
+/**
+ * Game controller sensor event structure (event.csensor.*)
+ */
+typedef struct SDL_ControllerSensorEvent
+{
+ Uint32 type; /**< SDL_CONTROLLERSENSORUPDATE */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_JoystickID which; /**< The joystick instance id */
+ Sint32 sensor; /**< The type of the sensor, one of the values of SDL_SensorType */
+ float data[3]; /**< Up to 3 values from the sensor, as defined in SDL_sensor.h */
+ Uint64 timestamp_us; /**< The timestamp of the sensor reading in microseconds, if the hardware provides this information. */
+} SDL_ControllerSensorEvent;
+
+/**
+ * Audio device event structure (event.adevice.*)
+ */
+typedef struct SDL_AudioDeviceEvent
+{
+ Uint32 type; /**< SDL_AUDIODEVICEADDED, or SDL_AUDIODEVICEREMOVED */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 which; /**< The audio device index for the ADDED event (valid until next SDL_GetNumAudioDevices() call), SDL_AudioDeviceID for the REMOVED event */
+ Uint8 iscapture; /**< zero if an output device, non-zero if a capture device. */
+ Uint8 padding1;
+ Uint8 padding2;
+ Uint8 padding3;
+} SDL_AudioDeviceEvent;
+
+
+/**
+ * Touch finger event structure (event.tfinger.*)
+ */
+typedef struct SDL_TouchFingerEvent
+{
+ Uint32 type; /**< SDL_FINGERMOTION or SDL_FINGERDOWN or SDL_FINGERUP */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_TouchID touchId; /**< The touch device id */
+ SDL_FingerID fingerId;
+ float x; /**< Normalized in the range 0...1 */
+ float y; /**< Normalized in the range 0...1 */
+ float dx; /**< Normalized in the range -1...1 */
+ float dy; /**< Normalized in the range -1...1 */
+ float pressure; /**< Normalized in the range 0...1 */
+ Uint32 windowID; /**< The window underneath the finger, if any */
+} SDL_TouchFingerEvent;
+
+
+/**
+ * Multiple Finger Gesture Event (event.mgesture.*)
+ */
+typedef struct SDL_MultiGestureEvent
+{
+ Uint32 type; /**< SDL_MULTIGESTURE */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_TouchID touchId; /**< The touch device id */
+ float dTheta;
+ float dDist;
+ float x;
+ float y;
+ Uint16 numFingers;
+ Uint16 padding;
+} SDL_MultiGestureEvent;
+
+
+/**
+ * Dollar Gesture Event (event.dgesture.*)
+ */
+typedef struct SDL_DollarGestureEvent
+{
+ Uint32 type; /**< SDL_DOLLARGESTURE or SDL_DOLLARRECORD */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_TouchID touchId; /**< The touch device id */
+ SDL_GestureID gestureId;
+ Uint32 numFingers;
+ float error;
+ float x; /**< Normalized center of gesture */
+ float y; /**< Normalized center of gesture */
+} SDL_DollarGestureEvent;
+
+
+/**
+ * An event used to request a file open by the system (event.drop.*)
+ *
+ * This event is enabled by default, you can disable it with SDL_EventState().
+ *
+ * If this event is enabled, you must free the filename in the event.
+ */
+typedef struct SDL_DropEvent
+{
+ Uint32 type; /**< SDL_DROPBEGIN or SDL_DROPFILE or SDL_DROPTEXT or SDL_DROPCOMPLETE */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ char *file; /**< The file name, which should be freed with SDL_free(), is NULL on begin/complete */
+ Uint32 windowID; /**< The window that was dropped on, if any */
+} SDL_DropEvent;
+
+
+/**
+ * Sensor event structure (event.sensor.*)
+ */
+typedef struct SDL_SensorEvent
+{
+ Uint32 type; /**< SDL_SENSORUPDATE */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Sint32 which; /**< The instance ID of the sensor */
+ float data[6]; /**< Up to 6 values from the sensor - additional values can be queried using SDL_SensorGetData() */
+ Uint64 timestamp_us; /**< The timestamp of the sensor reading in microseconds, if the hardware provides this information. */
+} SDL_SensorEvent;
+
+/**
+ * The "quit requested" event
+ */
+typedef struct SDL_QuitEvent
+{
+ Uint32 type; /**< SDL_QUIT */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+} SDL_QuitEvent;
+
+/**
+ * A user-defined event type (event.user.*)
+ */
+typedef struct SDL_UserEvent
+{
+ Uint32 type; /**< SDL_USEREVENT through SDL_LASTEVENT-1 */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ Uint32 windowID; /**< The associated window if any */
+ Sint32 code; /**< User defined event code */
+ void *data1; /**< User defined data pointer */
+ void *data2; /**< User defined data pointer */
+} SDL_UserEvent;
+
+
+struct SDL_SysWMmsg;
+typedef struct SDL_SysWMmsg SDL_SysWMmsg;
+
+/**
+ * A video driver dependent system event (event.syswm.*)
+ *
+ * This event is disabled by default, you can enable it with SDL_EventState()
+ *
+ * If you want to use this event, you should include SDL_syswm.h.
+ */
+typedef struct SDL_SysWMEvent
+{
+ Uint32 type; /**< SDL_SYSWMEVENT */
+ Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
+ SDL_SysWMmsg *msg; /**< driver dependent data, defined in SDL_syswm.h */
+} SDL_SysWMEvent;
+
+/**
+ * General event structure
+ *
+ * The SDL_Event structure is the core of all event handling in SDL. SDL_Event
+ * is a union of all event structures used in SDL.
+ */
+typedef union SDL_Event
+{
+ Uint32 type; /**< Event type, shared with all events */
+ SDL_CommonEvent common; /**< Common event data */
+ SDL_DisplayEvent display; /**< Display event data */
+ SDL_WindowEvent window; /**< Window event data */
+ SDL_KeyboardEvent key; /**< Keyboard event data */
+ SDL_TextEditingEvent edit; /**< Text editing event data */
+ SDL_TextEditingExtEvent editExt; /**< Extended text editing event data */
+ SDL_TextInputEvent text; /**< Text input event data */
+ SDL_MouseMotionEvent motion; /**< Mouse motion event data */
+ SDL_MouseButtonEvent button; /**< Mouse button event data */
+ SDL_MouseWheelEvent wheel; /**< Mouse wheel event data */
+ SDL_JoyAxisEvent jaxis; /**< Joystick axis event data */
+ SDL_JoyBallEvent jball; /**< Joystick ball event data */
+ SDL_JoyHatEvent jhat; /**< Joystick hat event data */
+ SDL_JoyButtonEvent jbutton; /**< Joystick button event data */
+ SDL_JoyDeviceEvent jdevice; /**< Joystick device change event data */
+ SDL_JoyBatteryEvent jbattery; /**< Joystick battery event data */
+ SDL_ControllerAxisEvent caxis; /**< Game Controller axis event data */
+ SDL_ControllerButtonEvent cbutton; /**< Game Controller button event data */
+ SDL_ControllerDeviceEvent cdevice; /**< Game Controller device event data */
+ SDL_ControllerTouchpadEvent ctouchpad; /**< Game Controller touchpad event data */
+ SDL_ControllerSensorEvent csensor; /**< Game Controller sensor event data */
+ SDL_AudioDeviceEvent adevice; /**< Audio device event data */
+ SDL_SensorEvent sensor; /**< Sensor event data */
+ SDL_QuitEvent quit; /**< Quit request event data */
+ SDL_UserEvent user; /**< Custom event data */
+ SDL_SysWMEvent syswm; /**< System dependent window event data */
+ SDL_TouchFingerEvent tfinger; /**< Touch finger event data */
+ SDL_MultiGestureEvent mgesture; /**< Gesture event data */
+ SDL_DollarGestureEvent dgesture; /**< Gesture event data */
+ SDL_DropEvent drop; /**< Drag and drop event data */
+
+ /* This is necessary for ABI compatibility between Visual C++ and GCC.
+ Visual C++ will respect the push pack pragma and use 52 bytes (size of
+ SDL_TextEditingEvent, the largest structure for 32-bit and 64-bit
+ architectures) for this union, and GCC will use the alignment of the
+ largest datatype within the union, which is 8 bytes on 64-bit
+ architectures.
+
+ So... we'll add padding to force the size to be 56 bytes for both.
+
+ On architectures where pointers are 16 bytes, this needs rounding up to
+ the next multiple of 16, 64, and on architectures where pointers are
+ even larger the size of SDL_UserEvent will dominate as being 3 pointers.
+ */
+ Uint8 padding[sizeof(void *) <= 8 ? 56 : sizeof(void *) == 16 ? 64 : 3 * sizeof(void *)];
+} SDL_Event;
+
+/* Make sure we haven't broken binary compatibility */
+SDL_COMPILE_TIME_ASSERT(SDL_Event, sizeof(SDL_Event) == sizeof(((SDL_Event *)NULL)->padding));
+
+
+/* Function prototypes */
+
+/**
+ * Pump the event loop, gathering events from the input devices.
+ *
+ * This function updates the event queue and internal input device state.
+ *
+ * **WARNING**: This should only be run in the thread that initialized the
+ * video subsystem, and for extra safety, you should consider only doing those
+ * things on the main thread in any case.
+ *
+ * SDL_PumpEvents() gathers all the pending input information from devices and
+ * places it in the event queue. Without calls to SDL_PumpEvents() no events
+ * would ever be placed on the queue. Often the need for calls to
+ * SDL_PumpEvents() is hidden from the user since SDL_PollEvent() and
+ * SDL_WaitEvent() implicitly call SDL_PumpEvents(). However, if you are not
+ * polling or waiting for events (e.g. you are filtering them), then you must
+ * call SDL_PumpEvents() to force an event queue update.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PollEvent
+ * \sa SDL_WaitEvent
+ */
+extern DECLSPEC void SDLCALL SDL_PumpEvents(void);
+
+/* @{ */
+typedef enum SDL_eventaction
+{
+ SDL_ADDEVENT,
+ SDL_PEEKEVENT,
+ SDL_GETEVENT
+} SDL_eventaction;
+
+/**
+ * Check the event queue for messages and optionally return them.
+ *
+ * `action` may be any of the following:
+ *
+ * - `SDL_ADDEVENT`: up to `numevents` events will be added to the back of the
+ * event queue.
+ * - `SDL_PEEKEVENT`: `numevents` events at the front of the event queue,
+ * within the specified minimum and maximum type, will be returned to the
+ * caller and will _not_ be removed from the queue.
+ * - `SDL_GETEVENT`: up to `numevents` events at the front of the event queue,
+ * within the specified minimum and maximum type, will be returned to the
+ * caller and will be removed from the queue.
+ *
+ * You may have to call SDL_PumpEvents() before calling this function.
+ * Otherwise, the events may not be ready to be filtered when you call
+ * SDL_PeepEvents().
+ *
+ * This function is thread-safe.
+ *
+ * \param events destination buffer for the retrieved events.
+ * \param numevents if action is SDL_ADDEVENT, the number of events to add
+ * back to the event queue; if action is SDL_PEEKEVENT or
+ * SDL_GETEVENT, the maximum number of events to retrieve.
+ * \param action action to take; see [[#action|Remarks]] for details.
+ * \param minType minimum value of the event type to be considered;
+ * SDL_FIRSTEVENT is a safe choice.
+ * \param maxType maximum value of the event type to be considered;
+ * SDL_LASTEVENT is a safe choice.
+ * \returns the number of events actually stored or a negative error code on
+ * failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PollEvent
+ * \sa SDL_PumpEvents
+ * \sa SDL_PushEvent
+ */
+extern DECLSPEC int SDLCALL SDL_PeepEvents(SDL_Event * events, int numevents,
+ SDL_eventaction action,
+ Uint32 minType, Uint32 maxType);
+/* @} */
+
+/**
+ * Check for the existence of a certain event type in the event queue.
+ *
+ * If you need to check for a range of event types, use SDL_HasEvents()
+ * instead.
+ *
+ * \param type the type of event to be queried; see SDL_EventType for details.
+ * \returns SDL_TRUE if events matching `type` are present, or SDL_FALSE if
+ * events matching `type` are not present.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HasEvents
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasEvent(Uint32 type);
+
+
+/**
+ * Check for the existence of certain event types in the event queue.
+ *
+ * If you need to check for a single event type, use SDL_HasEvent() instead.
+ *
+ * \param minType the low end of event type to be queried, inclusive; see
+ * SDL_EventType for details.
+ * \param maxType the high end of event type to be queried, inclusive; see
+ * SDL_EventType for details.
+ * \returns SDL_TRUE if events with type >= `minType` and <= `maxType` are
+ * present, or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HasEvents
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasEvents(Uint32 minType, Uint32 maxType);
+
+/**
+ * Clear events of a specific type from the event queue.
+ *
+ * This will unconditionally remove any events from the queue that match
+ * `type`. If you need to remove a range of event types, use SDL_FlushEvents()
+ * instead.
+ *
+ * It's also normal to just ignore events you don't care about in your event
+ * loop without calling this function.
+ *
+ * This function only affects currently queued events. If you want to make
+ * sure that all pending OS events are flushed, you can call SDL_PumpEvents()
+ * on the main thread immediately before the flush call.
+ *
+ * \param type the type of event to be cleared; see SDL_EventType for details.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_FlushEvents
+ */
+extern DECLSPEC void SDLCALL SDL_FlushEvent(Uint32 type);
+
+/**
+ * Clear events of a range of types from the event queue.
+ *
+ * This will unconditionally remove any events from the queue that are in the
+ * range of `minType` to `maxType`, inclusive. If you need to remove a single
+ * event type, use SDL_FlushEvent() instead.
+ *
+ * It's also normal to just ignore events you don't care about in your event
+ * loop without calling this function.
+ *
+ * This function only affects currently queued events. If you want to make
+ * sure that all pending OS events are flushed, you can call SDL_PumpEvents()
+ * on the main thread immediately before the flush call.
+ *
+ * \param minType the low end of event type to be cleared, inclusive; see
+ * SDL_EventType for details.
+ * \param maxType the high end of event type to be cleared, inclusive; see
+ * SDL_EventType for details.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_FlushEvent
+ */
+extern DECLSPEC void SDLCALL SDL_FlushEvents(Uint32 minType, Uint32 maxType);
+
+/**
+ * Poll for currently pending events.
+ *
+ * If `event` is not NULL, the next event is removed from the queue and stored
+ * in the SDL_Event structure pointed to by `event`. The 1 returned refers to
+ * this event, immediately stored in the SDL Event structure -- not an event
+ * to follow.
+ *
+ * If `event` is NULL, it simply returns 1 if there is an event in the queue,
+ * but will not remove it from the queue.
+ *
+ * As this function may implicitly call SDL_PumpEvents(), you can only call
+ * this function in the thread that set the video mode.
+ *
+ * SDL_PollEvent() is the favored way of receiving system events since it can
+ * be done from the main loop and does not suspend the main loop while waiting
+ * on an event to be posted.
+ *
+ * The common practice is to fully process the event queue once every frame,
+ * usually as a first step before updating the game's state:
+ *
+ * ```c
+ * while (game_is_still_running) {
+ * SDL_Event event;
+ * while (SDL_PollEvent(&event)) { // poll until all events are handled!
+ * // decide what to do with this event.
+ * }
+ *
+ * // update game state, draw the current frame
+ * }
+ * ```
+ *
+ * \param event the SDL_Event structure to be filled with the next event from
+ * the queue, or NULL.
+ * \returns 1 if there is a pending event or 0 if there are none available.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetEventFilter
+ * \sa SDL_PeepEvents
+ * \sa SDL_PushEvent
+ * \sa SDL_SetEventFilter
+ * \sa SDL_WaitEvent
+ * \sa SDL_WaitEventTimeout
+ */
+extern DECLSPEC int SDLCALL SDL_PollEvent(SDL_Event * event);
+
+/**
+ * Wait indefinitely for the next available event.
+ *
+ * If `event` is not NULL, the next event is removed from the queue and stored
+ * in the SDL_Event structure pointed to by `event`.
+ *
+ * As this function may implicitly call SDL_PumpEvents(), you can only call
+ * this function in the thread that initialized the video subsystem.
+ *
+ * \param event the SDL_Event structure to be filled in with the next event
+ * from the queue, or NULL.
+ * \returns 1 on success or 0 if there was an error while waiting for events;
+ * call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PollEvent
+ * \sa SDL_PumpEvents
+ * \sa SDL_WaitEventTimeout
+ */
+extern DECLSPEC int SDLCALL SDL_WaitEvent(SDL_Event * event);
+
+/**
+ * Wait until the specified timeout (in milliseconds) for the next available
+ * event.
+ *
+ * If `event` is not NULL, the next event is removed from the queue and stored
+ * in the SDL_Event structure pointed to by `event`.
+ *
+ * As this function may implicitly call SDL_PumpEvents(), you can only call
+ * this function in the thread that initialized the video subsystem.
+ *
+ * \param event the SDL_Event structure to be filled in with the next event
+ * from the queue, or NULL.
+ * \param timeout the maximum number of milliseconds to wait for the next
+ * available event.
+ * \returns 1 on success or 0 if there was an error while waiting for events;
+ * call SDL_GetError() for more information. This also returns 0 if
+ * the timeout elapsed without an event arriving.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PollEvent
+ * \sa SDL_PumpEvents
+ * \sa SDL_WaitEvent
+ */
+extern DECLSPEC int SDLCALL SDL_WaitEventTimeout(SDL_Event * event,
+ int timeout);
+
+/**
+ * Add an event to the event queue.
+ *
+ * The event queue can actually be used as a two way communication channel.
+ * Not only can events be read from the queue, but the user can also push
+ * their own events onto it. `event` is a pointer to the event structure you
+ * wish to push onto the queue. The event is copied into the queue, and the
+ * caller may dispose of the memory pointed to after SDL_PushEvent() returns.
+ *
+ * Note: Pushing device input events onto the queue doesn't modify the state
+ * of the device within SDL.
+ *
+ * This function is thread-safe, and can be called from other threads safely.
+ *
+ * Note: Events pushed onto the queue with SDL_PushEvent() get passed through
+ * the event filter but events added with SDL_PeepEvents() do not.
+ *
+ * For pushing application-specific events, please use SDL_RegisterEvents() to
+ * get an event type that does not conflict with other code that also wants
+ * its own custom event types.
+ *
+ * \param event the SDL_Event to be added to the queue.
+ * \returns 1 on success, 0 if the event was filtered, or a negative error
+ * code on failure; call SDL_GetError() for more information. A
+ * common reason for error is the event queue being full.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PeepEvents
+ * \sa SDL_PollEvent
+ * \sa SDL_RegisterEvents
+ */
+extern DECLSPEC int SDLCALL SDL_PushEvent(SDL_Event * event);
+
+/**
+ * A function pointer used for callbacks that watch the event queue.
+ *
+ * \param userdata what was passed as `userdata` to SDL_SetEventFilter() or
+ * SDL_AddEventWatch, etc.
+ * \param event the event that triggered the callback.
+ * \returns 1 to permit event to be added to the queue, and 0 to disallow it.
+ * When used with SDL_AddEventWatch, the return value is ignored.
+ *
+ * \sa SDL_SetEventFilter
+ * \sa SDL_AddEventWatch
+ */
+typedef int (SDLCALL * SDL_EventFilter) (void *userdata, SDL_Event * event);
+
+/**
+ * Set up a filter to process all events before they change internal state and
+ * are posted to the internal event queue.
+ *
+ * If the filter function returns 1 when called, then the event will be added
+ * to the internal queue. If it returns 0, then the event will be dropped from
+ * the queue, but the internal state will still be updated. This allows
+ * selective filtering of dynamically arriving events.
+ *
+ * **WARNING**: Be very careful of what you do in the event filter function,
+ * as it may run in a different thread!
+ *
+ * On platforms that support it, if the quit event is generated by an
+ * interrupt signal (e.g. pressing Ctrl-C), it will be delivered to the
+ * application at the next event poll.
+ *
+ * There is one caveat when dealing with the SDL_QuitEvent event type. The
+ * event filter is only called when the window manager desires to close the
+ * application window. If the event filter returns 1, then the window will be
+ * closed, otherwise the window will remain open if possible.
+ *
+ * Note: Disabled events never make it to the event filter function; see
+ * SDL_EventState().
+ *
+ * Note: If you just want to inspect events without filtering, you should use
+ * SDL_AddEventWatch() instead.
+ *
+ * Note: Events pushed onto the queue with SDL_PushEvent() get passed through
+ * the event filter, but events pushed onto the queue with SDL_PeepEvents() do
+ * not.
+ *
+ * \param filter An SDL_EventFilter function to call when an event happens.
+ * \param userdata a pointer that is passed to `filter`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AddEventWatch
+ * \sa SDL_EventState
+ * \sa SDL_GetEventFilter
+ * \sa SDL_PeepEvents
+ * \sa SDL_PushEvent
+ */
+extern DECLSPEC void SDLCALL SDL_SetEventFilter(SDL_EventFilter filter,
+ void *userdata);
+
+/**
+ * Query the current event filter.
+ *
+ * This function can be used to "chain" filters, by saving the existing filter
+ * before replacing it with a function that will call that saved filter.
+ *
+ * \param filter the current callback function will be stored here.
+ * \param userdata the pointer that is passed to the current event filter will
+ * be stored here.
+ * \returns SDL_TRUE on success or SDL_FALSE if there is no event filter set.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_SetEventFilter
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GetEventFilter(SDL_EventFilter * filter,
+ void **userdata);
+
+/**
+ * Add a callback to be triggered when an event is added to the event queue.
+ *
+ * `filter` will be called when an event happens, and its return value is
+ * ignored.
+ *
+ * **WARNING**: Be very careful of what you do in the event filter function,
+ * as it may run in a different thread!
+ *
+ * If the quit event is generated by a signal (e.g. SIGINT), it will bypass
+ * the internal queue and be delivered to the watch callback immediately, and
+ * arrive at the next event poll.
+ *
+ * Note: the callback is called for events posted by the user through
+ * SDL_PushEvent(), but not for disabled events, nor for events by a filter
+ * callback set with SDL_SetEventFilter(), nor for events posted by the user
+ * through SDL_PeepEvents().
+ *
+ * \param filter an SDL_EventFilter function to call when an event happens.
+ * \param userdata a pointer that is passed to `filter`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_DelEventWatch
+ * \sa SDL_SetEventFilter
+ */
+extern DECLSPEC void SDLCALL SDL_AddEventWatch(SDL_EventFilter filter,
+ void *userdata);
+
+/**
+ * Remove an event watch callback added with SDL_AddEventWatch().
+ *
+ * This function takes the same input as SDL_AddEventWatch() to identify and
+ * delete the corresponding callback.
+ *
+ * \param filter the function originally passed to SDL_AddEventWatch().
+ * \param userdata the pointer originally passed to SDL_AddEventWatch().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AddEventWatch
+ */
+extern DECLSPEC void SDLCALL SDL_DelEventWatch(SDL_EventFilter filter,
+ void *userdata);
+
+/**
+ * Run a specific filter function on the current event queue, removing any
+ * events for which the filter returns 0.
+ *
+ * See SDL_SetEventFilter() for more information. Unlike SDL_SetEventFilter(),
+ * this function does not change the filter permanently, it only uses the
+ * supplied filter until this function returns.
+ *
+ * \param filter the SDL_EventFilter function to call when an event happens.
+ * \param userdata a pointer that is passed to `filter`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetEventFilter
+ * \sa SDL_SetEventFilter
+ */
+extern DECLSPEC void SDLCALL SDL_FilterEvents(SDL_EventFilter filter,
+ void *userdata);
+
+/* @{ */
+#define SDL_QUERY -1
+#define SDL_IGNORE 0
+#define SDL_DISABLE 0
+#define SDL_ENABLE 1
+
+/**
+ * Set the state of processing events by type.
+ *
+ * `state` may be any of the following:
+ *
+ * - `SDL_QUERY`: returns the current processing state of the specified event
+ * - `SDL_IGNORE` (aka `SDL_DISABLE`): the event will automatically be dropped
+ * from the event queue and will not be filtered
+ * - `SDL_ENABLE`: the event will be processed normally
+ *
+ * \param type the type of event; see SDL_EventType for details.
+ * \param state how to process the event.
+ * \returns `SDL_DISABLE` or `SDL_ENABLE`, representing the processing state
+ * of the event before this function makes any changes to it.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetEventState
+ */
+extern DECLSPEC Uint8 SDLCALL SDL_EventState(Uint32 type, int state);
+/* @} */
+#define SDL_GetEventState(type) SDL_EventState(type, SDL_QUERY)
+
+/**
+ * Allocate a set of user-defined events, and return the beginning event
+ * number for that set of events.
+ *
+ * Calling this function with `numevents` <= 0 is an error and will return
+ * (Uint32)-1.
+ *
+ * Note, (Uint32)-1 means the maximum unsigned 32-bit integer value (or
+ * 0xFFFFFFFF), but is clearer to write.
+ *
+ * \param numevents the number of events to be allocated.
+ * \returns the beginning event number, or (Uint32)-1 if there are not enough
+ * user-defined events left.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PushEvent
+ */
+extern DECLSPEC Uint32 SDLCALL SDL_RegisterEvents(int numevents);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_events_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_filesystem.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_filesystem.h
new file mode 100644
index 00000000..c72a6165
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_filesystem.h
@@ -0,0 +1,149 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryFilesystem
+ *
+ * Include file for filesystem SDL API functions
+ */
+
+#ifndef SDL_filesystem_h_
+#define SDL_filesystem_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Get the directory where the application was run from.
+ *
+ * This is not necessarily a fast call, so you should call this once near
+ * startup and save the string if you need it.
+ *
+ * **Mac OS X and iOS Specific Functionality**: If the application is in a
+ * ".app" bundle, this function returns the Resource directory (e.g.
+ * MyApp.app/Contents/Resources/). This behaviour can be overridden by adding
+ * a property to the Info.plist file. Adding a string key with the name
+ * SDL_FILESYSTEM_BASE_DIR_TYPE with a supported value will change the
+ * behaviour.
+ *
+ * Supported values for the SDL_FILESYSTEM_BASE_DIR_TYPE property (Given an
+ * application in /Applications/SDLApp/MyApp.app):
+ *
+ * - `resource`: bundle resource directory (the default). For example:
+ * `/Applications/SDLApp/MyApp.app/Contents/Resources`
+ * - `bundle`: the Bundle directory. For example:
+ * `/Applications/SDLApp/MyApp.app/`
+ * - `parent`: the containing directory of the bundle. For example:
+ * `/Applications/SDLApp/`
+ *
+ * **Nintendo 3DS Specific Functionality**: This function returns "romfs"
+ * directory of the application as it is uncommon to store resources outside
+ * the executable. As such it is not a writable directory.
+ *
+ * The returned path is guaranteed to end with a path separator ('\\' on
+ * Windows, '/' on most other platforms).
+ *
+ * The pointer returned is owned by the caller. Please call SDL_free() on the
+ * pointer when done with it.
+ *
+ * \returns an absolute path in UTF-8 encoding to the application data
+ * directory. NULL will be returned on error or when the platform
+ * doesn't implement this functionality, call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.1.
+ *
+ * \sa SDL_GetPrefPath
+ */
+extern DECLSPEC char *SDLCALL SDL_GetBasePath(void);
+
+/**
+ * Get the user-and-app-specific path where files can be written.
+ *
+ * Get the "pref dir". This is meant to be where users can write personal
+ * files (preferences and save games, etc) that are specific to your
+ * application. This directory is unique per user, per application.
+ *
+ * This function will decide the appropriate location in the native
+ * filesystem, create the directory if necessary, and return a string of the
+ * absolute path to the directory in UTF-8 encoding.
+ *
+ * On Windows, the string might look like:
+ *
+ * `C:\\Users\\bob\\AppData\\Roaming\\My Company\\My Program Name\\`
+ *
+ * On Linux, the string might look like:
+ *
+ * `/home/bob/.local/share/My Program Name/`
+ *
+ * On Mac OS X, the string might look like:
+ *
+ * `/Users/bob/Library/Application Support/My Program Name/`
+ *
+ * You should assume the path returned by this function is the only safe place
+ * to write files (and that SDL_GetBasePath(), while it might be writable, or
+ * even the parent of the returned path, isn't where you should be writing
+ * things).
+ *
+ * Both the org and app strings may become part of a directory name, so please
+ * follow these rules:
+ *
+ * - Try to use the same org string (_including case-sensitivity_) for all
+ * your applications that use this function.
+ * - Always use a unique app string for each one, and make sure it never
+ * changes for an app once you've decided on it.
+ * - Unicode characters are legal, as long as it's UTF-8 encoded, but...
+ * - ...only use letters, numbers, and spaces. Avoid punctuation like "Game
+ * Name 2: Bad Guy's Revenge!" ... "Game Name 2" is sufficient.
+ *
+ * The returned path is guaranteed to end with a path separator ('\\' on
+ * Windows, '/' on most other platforms).
+ *
+ * The pointer returned is owned by the caller. Please call SDL_free() on the
+ * pointer when done with it.
+ *
+ * \param org the name of your organization.
+ * \param app the name of your application.
+ * \returns a UTF-8 string of the user directory in platform-dependent
+ * notation. NULL if there's a problem (creating directory failed,
+ * etc.).
+ *
+ * \since This function is available since SDL 2.0.1.
+ *
+ * \sa SDL_GetBasePath
+ */
+extern DECLSPEC char *SDLCALL SDL_GetPrefPath(const char *org, const char *app);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_filesystem_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_gamecontroller.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_gamecontroller.h
new file mode 100644
index 00000000..4d8bcce2
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_gamecontroller.h
@@ -0,0 +1,1110 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* WIKI CATEGORY: GameController */
+
+/**
+ * # CategoryGameController
+ *
+ * Include file for SDL game controller event handling
+ */
+
+#ifndef SDL_gamecontroller_h_
+#define SDL_gamecontroller_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+#include "SDL_rwops.h"
+#include "SDL_sensor.h"
+#include "SDL_joystick.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \file SDL_gamecontroller.h
+ *
+ * In order to use these functions, SDL_Init() must have been called
+ * with the SDL_INIT_GAMECONTROLLER flag. This causes SDL to scan the system
+ * for game controllers, and load appropriate drivers.
+ *
+ * If you would like to receive controller updates while the application
+ * is in the background, you should set the following hint before calling
+ * SDL_Init(): SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS
+ */
+
+/**
+ * The gamecontroller structure used to identify an SDL game controller
+ */
+struct _SDL_GameController;
+typedef struct _SDL_GameController SDL_GameController;
+
+typedef enum SDL_GameControllerType
+{
+ SDL_CONTROLLER_TYPE_UNKNOWN = 0,
+ SDL_CONTROLLER_TYPE_XBOX360,
+ SDL_CONTROLLER_TYPE_XBOXONE,
+ SDL_CONTROLLER_TYPE_PS3,
+ SDL_CONTROLLER_TYPE_PS4,
+ SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO,
+ SDL_CONTROLLER_TYPE_VIRTUAL,
+ SDL_CONTROLLER_TYPE_PS5,
+ SDL_CONTROLLER_TYPE_AMAZON_LUNA,
+ SDL_CONTROLLER_TYPE_GOOGLE_STADIA,
+ SDL_CONTROLLER_TYPE_NVIDIA_SHIELD,
+ SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT,
+ SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT,
+ SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR,
+ SDL_CONTROLLER_TYPE_MAX
+} SDL_GameControllerType;
+
+typedef enum SDL_GameControllerBindType
+{
+ SDL_CONTROLLER_BINDTYPE_NONE = 0,
+ SDL_CONTROLLER_BINDTYPE_BUTTON,
+ SDL_CONTROLLER_BINDTYPE_AXIS,
+ SDL_CONTROLLER_BINDTYPE_HAT
+} SDL_GameControllerBindType;
+
+/**
+ * Get the SDL joystick layer binding for this controller button/axis mapping
+ */
+typedef struct SDL_GameControllerButtonBind
+{
+ SDL_GameControllerBindType bindType;
+ union
+ {
+ int button;
+ int axis;
+ struct {
+ int hat;
+ int hat_mask;
+ } hat;
+ } value;
+
+} SDL_GameControllerButtonBind;
+
+
+/**
+ * To count the number of game controllers in the system for the following:
+ *
+ * ```c
+ * int nJoysticks = SDL_NumJoysticks();
+ * int nGameControllers = 0;
+ * for (int i = 0; i < nJoysticks; i++) {
+ * if (SDL_IsGameController(i)) {
+ * nGameControllers++;
+ * }
+ * }
+ * ```
+ *
+ * Using the SDL_HINT_GAMECONTROLLERCONFIG hint or the SDL_GameControllerAddMapping() you can add support for controllers SDL is unaware of or cause an existing controller to have a different binding. The format is:
+ * guid,name,mappings
+ *
+ * Where GUID is the string value from SDL_JoystickGetGUIDString(), name is the human readable string for the device and mappings are controller mappings to joystick ones.
+ * Under Windows there is a reserved GUID of "xinput" that covers any XInput devices.
+ * The mapping format for joystick is:
+ * bX - a joystick button, index X
+ * hX.Y - hat X with value Y
+ * aX - axis X of the joystick
+ * Buttons can be used as a controller axis and vice versa.
+ *
+ * This string shows an example of a valid mapping for a controller
+ *
+ * ```c
+ * "03000000341a00003608000000000000,PS3 Controller,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftshoulder:b4,rightshoulder:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7",
+ * ```
+ */
+
+/**
+ * Load a set of Game Controller mappings from a seekable SDL data stream.
+ *
+ * You can call this function several times, if needed, to load different
+ * database files.
+ *
+ * If a new mapping is loaded for an already known controller GUID, the later
+ * version will overwrite the one currently loaded.
+ *
+ * If this function is called before SDL_Init, SDL will generate an
+ * SDL_CONTROLLERDEVICEADDED event for matching controllers that are plugged
+ * in at the time that SDL_Init is called.
+ *
+ * Mappings not belonging to the current platform or with no platform field
+ * specified will be ignored (i.e. mappings for Linux will be ignored in
+ * Windows, etc).
+ *
+ * This function will load the text database entirely in memory before
+ * processing it, so take this into consideration if you are in a memory
+ * constrained environment.
+ *
+ * \param rw the data stream for the mappings to be added.
+ * \param freerw non-zero to close the stream after being read.
+ * \returns the number of mappings added or -1 on error; call SDL_GetError()
+ * for more information.
+ *
+ * \since This function is available since SDL 2.0.2.
+ *
+ * \sa SDL_GameControllerAddMapping
+ * \sa SDL_GameControllerAddMappingsFromFile
+ * \sa SDL_GameControllerMappingForGUID
+ * \sa SDL_CONTROLLERDEVICEADDED
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerAddMappingsFromRW(SDL_RWops * rw, int freerw);
+
+/**
+ * Load a set of mappings from a file, filtered by the current
+ * SDL_GetPlatform()
+ *
+ * Convenience macro.
+ */
+#define SDL_GameControllerAddMappingsFromFile(file) SDL_GameControllerAddMappingsFromRW(SDL_RWFromFile(file, "rb"), 1)
+
+/**
+ * Add support for controllers that SDL is unaware of or to cause an existing
+ * controller to have a different binding.
+ *
+ * The mapping string has the format "GUID,name,mapping", where GUID is the
+ * string value from SDL_JoystickGetGUIDString(), name is the human readable
+ * string for the device and mappings are controller mappings to joystick
+ * ones. Under Windows there is a reserved GUID of "xinput" that covers all
+ * XInput devices. The mapping format for joystick is: {| |bX |a joystick
+ * button, index X |- |hX.Y |hat X with value Y |- |aX |axis X of the joystick
+ * |} Buttons can be used as a controller axes and vice versa.
+ *
+ * This string shows an example of a valid mapping for a controller:
+ *
+ * ```c
+ * "341a3608000000000000504944564944,Afterglow PS3 Controller,a:b1,b:b2,y:b3,x:b0,start:b9,guide:b12,back:b8,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,leftshoulder:b4,rightshoulder:b5,leftstick:b10,rightstick:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7"
+ * ```
+ *
+ * If this function is called before SDL_Init, SDL will generate an
+ * SDL_CONTROLLERDEVICEADDED event for matching controllers that are plugged
+ * in at the time that SDL_Init is called.
+ *
+ * \param mappingString the mapping string.
+ * \returns 1 if a new mapping is added, 0 if an existing mapping is updated,
+ * -1 on error; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerMapping
+ * \sa SDL_GameControllerMappingForGUID
+ * \sa SDL_CONTROLLERDEVICEADDED
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerAddMapping(const char* mappingString);
+
+/**
+ * Get the number of mappings installed.
+ *
+ * \returns the number of mappings.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerNumMappings(void);
+
+/**
+ * Get the mapping at a particular index.
+ *
+ * \returns the mapping string. Must be freed with SDL_free(). Returns NULL if
+ * the index is out of range.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC char * SDLCALL SDL_GameControllerMappingForIndex(int mapping_index);
+
+/**
+ * Get the game controller mapping string for a given GUID.
+ *
+ * The returned string must be freed with SDL_free().
+ *
+ * \param guid a structure containing the GUID for which a mapping is desired.
+ * \returns a mapping string or NULL on error; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetDeviceGUID
+ * \sa SDL_JoystickGetGUID
+ */
+extern DECLSPEC char * SDLCALL SDL_GameControllerMappingForGUID(SDL_JoystickGUID guid);
+
+/**
+ * Get the current mapping of a Game Controller.
+ *
+ * The returned string must be freed with SDL_free().
+ *
+ * Details about mappings are discussed with SDL_GameControllerAddMapping().
+ *
+ * \param gamecontroller the game controller you want to get the current
+ * mapping for.
+ * \returns a string that has the controller's mapping or NULL if no mapping
+ * is available; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerAddMapping
+ * \sa SDL_GameControllerMappingForGUID
+ */
+extern DECLSPEC char * SDLCALL SDL_GameControllerMapping(SDL_GameController *gamecontroller);
+
+/**
+ * Check if the given joystick is supported by the game controller interface.
+ *
+ * `joystick_index` is the same as the `device_index` passed to
+ * SDL_JoystickOpen().
+ *
+ * \param joystick_index the device_index of a device, up to
+ * SDL_NumJoysticks().
+ * \returns SDL_TRUE if the given joystick is supported by the game controller
+ * interface, SDL_FALSE if it isn't or it's an invalid index.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerNameForIndex
+ * \sa SDL_GameControllerOpen
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_IsGameController(int joystick_index);
+
+/**
+ * Get the implementation dependent name for the game controller.
+ *
+ * This function can be called before any controllers are opened.
+ *
+ * `joystick_index` is the same as the `device_index` passed to
+ * SDL_JoystickOpen().
+ *
+ * \param joystick_index the device_index of a device, from zero to
+ * SDL_NumJoysticks()-1.
+ * \returns the implementation-dependent name for the game controller, or NULL
+ * if there is no name or the index is invalid.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerName
+ * \sa SDL_GameControllerOpen
+ * \sa SDL_IsGameController
+ */
+extern DECLSPEC const char *SDLCALL SDL_GameControllerNameForIndex(int joystick_index);
+
+/**
+ * Get the implementation dependent path for the game controller.
+ *
+ * This function can be called before any controllers are opened.
+ *
+ * `joystick_index` is the same as the `device_index` passed to
+ * SDL_JoystickOpen().
+ *
+ * \param joystick_index the device_index of a device, from zero to
+ * SDL_NumJoysticks()-1.
+ * \returns the implementation-dependent path for the game controller, or NULL
+ * if there is no path or the index is invalid.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GameControllerPath
+ */
+extern DECLSPEC const char *SDLCALL SDL_GameControllerPathForIndex(int joystick_index);
+
+/**
+ * Get the type of a game controller.
+ *
+ * This can be called before any controllers are opened.
+ *
+ * \param joystick_index the device_index of a device, from zero to
+ * SDL_NumJoysticks()-1.
+ * \returns the controller type.
+ *
+ * \since This function is available since SDL 2.0.12.
+ */
+extern DECLSPEC SDL_GameControllerType SDLCALL SDL_GameControllerTypeForIndex(int joystick_index);
+
+/**
+ * Get the mapping of a game controller.
+ *
+ * This can be called before any controllers are opened.
+ *
+ * \param joystick_index the device_index of a device, from zero to
+ * SDL_NumJoysticks()-1.
+ * \returns the mapping string. Must be freed with SDL_free(). Returns NULL if
+ * no mapping is available.
+ *
+ * \since This function is available since SDL 2.0.9.
+ */
+extern DECLSPEC char *SDLCALL SDL_GameControllerMappingForDeviceIndex(int joystick_index);
+
+/**
+ * Open a game controller for use.
+ *
+ * `joystick_index` is the same as the `device_index` passed to
+ * SDL_JoystickOpen().
+ *
+ * The index passed as an argument refers to the N'th game controller on the
+ * system. This index is not the value which will identify this controller in
+ * future controller events. The joystick's instance id (SDL_JoystickID) will
+ * be used there instead.
+ *
+ * \param joystick_index the device_index of a device, up to
+ * SDL_NumJoysticks().
+ * \returns a gamecontroller identifier or NULL if an error occurred; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerClose
+ * \sa SDL_GameControllerNameForIndex
+ * \sa SDL_IsGameController
+ */
+extern DECLSPEC SDL_GameController *SDLCALL SDL_GameControllerOpen(int joystick_index);
+
+/**
+ * Get the SDL_GameController associated with an instance id.
+ *
+ * \param joyid the instance id to get the SDL_GameController for.
+ * \returns an SDL_GameController on success or NULL on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.4.
+ */
+extern DECLSPEC SDL_GameController *SDLCALL SDL_GameControllerFromInstanceID(SDL_JoystickID joyid);
+
+/**
+ * Get the SDL_GameController associated with a player index.
+ *
+ * Please note that the player index is _not_ the device index, nor is it the
+ * instance id!
+ *
+ * \param player_index the player index, which is not the device index or the
+ * instance id!
+ * \returns the SDL_GameController associated with a player index.
+ *
+ * \since This function is available since SDL 2.0.12.
+ *
+ * \sa SDL_GameControllerGetPlayerIndex
+ * \sa SDL_GameControllerSetPlayerIndex
+ */
+extern DECLSPEC SDL_GameController *SDLCALL SDL_GameControllerFromPlayerIndex(int player_index);
+
+/**
+ * Get the implementation-dependent name for an opened game controller.
+ *
+ * This is the same name as returned by SDL_GameControllerNameForIndex(), but
+ * it takes a controller identifier instead of the (unstable) device index.
+ *
+ * \param gamecontroller a game controller identifier previously returned by
+ * SDL_GameControllerOpen().
+ * \returns the implementation dependent name for the game controller, or NULL
+ * if there is no name or the identifier passed is invalid.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerNameForIndex
+ * \sa SDL_GameControllerOpen
+ */
+extern DECLSPEC const char *SDLCALL SDL_GameControllerName(SDL_GameController *gamecontroller);
+
+/**
+ * Get the implementation-dependent path for an opened game controller.
+ *
+ * This is the same path as returned by SDL_GameControllerNameForIndex(), but
+ * it takes a controller identifier instead of the (unstable) device index.
+ *
+ * \param gamecontroller a game controller identifier previously returned by
+ * SDL_GameControllerOpen().
+ * \returns the implementation dependent path for the game controller, or NULL
+ * if there is no path or the identifier passed is invalid.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GameControllerPathForIndex
+ */
+extern DECLSPEC const char *SDLCALL SDL_GameControllerPath(SDL_GameController *gamecontroller);
+
+/**
+ * Get the type of this currently opened controller
+ *
+ * This is the same name as returned by SDL_GameControllerTypeForIndex(), but
+ * it takes a controller identifier instead of the (unstable) device index.
+ *
+ * \param gamecontroller the game controller object to query.
+ * \returns the controller type.
+ *
+ * \since This function is available since SDL 2.0.12.
+ */
+extern DECLSPEC SDL_GameControllerType SDLCALL SDL_GameControllerGetType(SDL_GameController *gamecontroller);
+
+/**
+ * Get the player index of an opened game controller.
+ *
+ * For XInput controllers this returns the XInput user index.
+ *
+ * \param gamecontroller the game controller object to query.
+ * \returns the player index for controller, or -1 if it's not available.
+ *
+ * \since This function is available since SDL 2.0.9.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerGetPlayerIndex(SDL_GameController *gamecontroller);
+
+/**
+ * Set the player index of an opened game controller.
+ *
+ * \param gamecontroller the game controller object to adjust.
+ * \param player_index Player index to assign to this controller, or -1 to
+ * clear the player index and turn off player LEDs.
+ *
+ * \since This function is available since SDL 2.0.12.
+ */
+extern DECLSPEC void SDLCALL SDL_GameControllerSetPlayerIndex(SDL_GameController *gamecontroller, int player_index);
+
+/**
+ * Get the USB vendor ID of an opened controller, if available.
+ *
+ * If the vendor ID isn't available this function returns 0.
+ *
+ * \param gamecontroller the game controller object to query.
+ * \return the USB vendor ID, or zero if unavailable.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_GameControllerGetVendor(SDL_GameController *gamecontroller);
+
+/**
+ * Get the USB product ID of an opened controller, if available.
+ *
+ * If the product ID isn't available this function returns 0.
+ *
+ * \param gamecontroller the game controller object to query.
+ * \return the USB product ID, or zero if unavailable.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_GameControllerGetProduct(SDL_GameController *gamecontroller);
+
+/**
+ * Get the product version of an opened controller, if available.
+ *
+ * If the product version isn't available this function returns 0.
+ *
+ * \param gamecontroller the game controller object to query.
+ * \return the USB product version, or zero if unavailable.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_GameControllerGetProductVersion(SDL_GameController *gamecontroller);
+
+/**
+ * Get the firmware version of an opened controller, if available.
+ *
+ * If the firmware version isn't available this function returns 0.
+ *
+ * \param gamecontroller the game controller object to query.
+ * \return the controller firmware version, or zero if unavailable.
+ *
+ * \since This function is available since SDL 2.24.0.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_GameControllerGetFirmwareVersion(SDL_GameController *gamecontroller);
+
+/**
+ * Get the serial number of an opened controller, if available.
+ *
+ * Returns the serial number of the controller, or NULL if it is not
+ * available.
+ *
+ * \param gamecontroller the game controller object to query.
+ * \return the serial number, or NULL if unavailable.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC const char * SDLCALL SDL_GameControllerGetSerial(SDL_GameController *gamecontroller);
+
+/**
+ * Get the Steam Input handle of an opened controller, if available.
+ *
+ * Returns an InputHandle_t for the controller that can be used with Steam
+ * Input API: https://partner.steamgames.com/doc/api/ISteamInput
+ *
+ * \param gamecontroller the game controller object to query.
+ * \returns the gamepad handle, or 0 if unavailable.
+ *
+ * \since This function is available since SDL 2.30.0.
+ */
+extern DECLSPEC Uint64 SDLCALL SDL_GameControllerGetSteamHandle(SDL_GameController *gamecontroller);
+
+
+/**
+ * Check if a controller has been opened and is currently connected.
+ *
+ * \param gamecontroller a game controller identifier previously returned by
+ * SDL_GameControllerOpen().
+ * \returns SDL_TRUE if the controller has been opened and is currently
+ * connected, or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerClose
+ * \sa SDL_GameControllerOpen
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GameControllerGetAttached(SDL_GameController *gamecontroller);
+
+/**
+ * Get the Joystick ID from a Game Controller.
+ *
+ * This function will give you a SDL_Joystick object, which allows you to use
+ * the SDL_Joystick functions with a SDL_GameController object. This would be
+ * useful for getting a joystick's position at any given time, even if it
+ * hasn't moved (moving it would produce an event, which would have the axis'
+ * value).
+ *
+ * The pointer returned is owned by the SDL_GameController. You should not
+ * call SDL_JoystickClose() on it, for example, since doing so will likely
+ * cause SDL to crash.
+ *
+ * \param gamecontroller the game controller object that you want to get a
+ * joystick from.
+ * \returns a SDL_Joystick object; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC SDL_Joystick *SDLCALL SDL_GameControllerGetJoystick(SDL_GameController *gamecontroller);
+
+/**
+ * Query or change current state of Game Controller events.
+ *
+ * If controller events are disabled, you must call SDL_GameControllerUpdate()
+ * yourself and check the state of the controller when you want controller
+ * information.
+ *
+ * Any number can be passed to SDL_GameControllerEventState(), but only -1, 0,
+ * and 1 will have any effect. Other numbers will just be returned.
+ *
+ * \param state can be one of `SDL_QUERY`, `SDL_IGNORE`, or `SDL_ENABLE`.
+ * \returns the same value passed to the function, with exception to -1
+ * (SDL_QUERY), which will return the current state.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickEventState
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerEventState(int state);
+
+/**
+ * Manually pump game controller updates if not using the loop.
+ *
+ * This function is called automatically by the event loop if events are
+ * enabled. Under such circumstances, it will not be necessary to call this
+ * function.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC void SDLCALL SDL_GameControllerUpdate(void);
+
+
+/**
+ * The list of axes available from a controller
+ *
+ * Thumbstick axis values range from SDL_JOYSTICK_AXIS_MIN to
+ * SDL_JOYSTICK_AXIS_MAX, and are centered within ~8000 of zero, though
+ * advanced UI will allow users to set or autodetect the dead zone, which
+ * varies between controllers.
+ *
+ * Trigger axis values range from 0 (released) to SDL_JOYSTICK_AXIS_MAX (fully
+ * pressed) when reported by SDL_GameControllerGetAxis(). Note that this is
+ * not the same range that will be reported by the lower-level
+ * SDL_GetJoystickAxis().
+ */
+typedef enum SDL_GameControllerAxis
+{
+ SDL_CONTROLLER_AXIS_INVALID = -1,
+ SDL_CONTROLLER_AXIS_LEFTX,
+ SDL_CONTROLLER_AXIS_LEFTY,
+ SDL_CONTROLLER_AXIS_RIGHTX,
+ SDL_CONTROLLER_AXIS_RIGHTY,
+ SDL_CONTROLLER_AXIS_TRIGGERLEFT,
+ SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
+ SDL_CONTROLLER_AXIS_MAX
+} SDL_GameControllerAxis;
+
+/**
+ * Convert a string into SDL_GameControllerAxis enum.
+ *
+ * This function is called internally to translate SDL_GameController mapping
+ * strings for the underlying joystick device into the consistent
+ * SDL_GameController mapping. You do not normally need to call this function
+ * unless you are parsing SDL_GameController mappings in your own code.
+ *
+ * Note specially that "righttrigger" and "lefttrigger" map to
+ * `SDL_CONTROLLER_AXIS_TRIGGERRIGHT` and `SDL_CONTROLLER_AXIS_TRIGGERLEFT`,
+ * respectively.
+ *
+ * \param str string representing a SDL_GameController axis.
+ * \returns the SDL_GameControllerAxis enum corresponding to the input string,
+ * or `SDL_CONTROLLER_AXIS_INVALID` if no match was found.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerGetStringForAxis
+ */
+extern DECLSPEC SDL_GameControllerAxis SDLCALL SDL_GameControllerGetAxisFromString(const char *str);
+
+/**
+ * Convert from an SDL_GameControllerAxis enum to a string.
+ *
+ * The caller should not SDL_free() the returned string.
+ *
+ * \param axis an enum value for a given SDL_GameControllerAxis.
+ * \returns a string for the given axis, or NULL if an invalid axis is
+ * specified. The string returned is of the format used by
+ * SDL_GameController mapping strings.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerGetAxisFromString
+ */
+extern DECLSPEC const char* SDLCALL SDL_GameControllerGetStringForAxis(SDL_GameControllerAxis axis);
+
+/**
+ * Get the SDL joystick layer binding for a controller axis mapping.
+ *
+ * \param gamecontroller a game controller.
+ * \param axis an axis enum value (one of the SDL_GameControllerAxis values).
+ * \returns a SDL_GameControllerButtonBind describing the bind. On failure
+ * (like the given Controller axis doesn't exist on the device), its
+ * `.bindType` will be `SDL_CONTROLLER_BINDTYPE_NONE`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerGetBindForButton
+ */
+extern DECLSPEC SDL_GameControllerButtonBind SDLCALL
+SDL_GameControllerGetBindForAxis(SDL_GameController *gamecontroller,
+ SDL_GameControllerAxis axis);
+
+/**
+ * Query whether a game controller has a given axis.
+ *
+ * This merely reports whether the controller's mapping defined this axis, as
+ * that is all the information SDL has about the physical device.
+ *
+ * \param gamecontroller a game controller.
+ * \param axis an axis enum value (an SDL_GameControllerAxis value).
+ * \returns SDL_TRUE if the controller has this axis, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_bool SDLCALL
+SDL_GameControllerHasAxis(SDL_GameController *gamecontroller, SDL_GameControllerAxis axis);
+
+/**
+ * Get the current state of an axis control on a game controller.
+ *
+ * The axis indices start at index 0.
+ *
+ * For thumbsticks, the state is a value ranging from -32768 (up/left) to
+ * 32767 (down/right).
+ *
+ * Triggers range from 0 when released to 32767 when fully pressed, and never
+ * return a negative value. Note that this differs from the value reported by
+ * the lower-level SDL_JoystickGetAxis(), which normally uses the full range.
+ *
+ * \param gamecontroller a game controller.
+ * \param axis an axis index (one of the SDL_GameControllerAxis values).
+ * \returns axis state (including 0) on success or 0 (also) on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerGetButton
+ */
+extern DECLSPEC Sint16 SDLCALL
+SDL_GameControllerGetAxis(SDL_GameController *gamecontroller, SDL_GameControllerAxis axis);
+
+/**
+ * The list of buttons available from a controller
+ */
+typedef enum SDL_GameControllerButton
+{
+ SDL_CONTROLLER_BUTTON_INVALID = -1,
+ SDL_CONTROLLER_BUTTON_A,
+ SDL_CONTROLLER_BUTTON_B,
+ SDL_CONTROLLER_BUTTON_X,
+ SDL_CONTROLLER_BUTTON_Y,
+ SDL_CONTROLLER_BUTTON_BACK,
+ SDL_CONTROLLER_BUTTON_GUIDE,
+ SDL_CONTROLLER_BUTTON_START,
+ SDL_CONTROLLER_BUTTON_LEFTSTICK,
+ SDL_CONTROLLER_BUTTON_RIGHTSTICK,
+ SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
+ SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
+ SDL_CONTROLLER_BUTTON_DPAD_UP,
+ SDL_CONTROLLER_BUTTON_DPAD_DOWN,
+ SDL_CONTROLLER_BUTTON_DPAD_LEFT,
+ SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
+ SDL_CONTROLLER_BUTTON_MISC1, /* Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button */
+ SDL_CONTROLLER_BUTTON_PADDLE1, /* Xbox Elite paddle P1 (upper left, facing the back) */
+ SDL_CONTROLLER_BUTTON_PADDLE2, /* Xbox Elite paddle P3 (upper right, facing the back) */
+ SDL_CONTROLLER_BUTTON_PADDLE3, /* Xbox Elite paddle P2 (lower left, facing the back) */
+ SDL_CONTROLLER_BUTTON_PADDLE4, /* Xbox Elite paddle P4 (lower right, facing the back) */
+ SDL_CONTROLLER_BUTTON_TOUCHPAD, /* PS4/PS5 touchpad button */
+ SDL_CONTROLLER_BUTTON_MAX
+} SDL_GameControllerButton;
+
+/**
+ * Convert a string into an SDL_GameControllerButton enum.
+ *
+ * This function is called internally to translate SDL_GameController mapping
+ * strings for the underlying joystick device into the consistent
+ * SDL_GameController mapping. You do not normally need to call this function
+ * unless you are parsing SDL_GameController mappings in your own code.
+ *
+ * \param str string representing a SDL_GameController axis.
+ * \returns the SDL_GameControllerButton enum corresponding to the input
+ * string, or `SDL_CONTROLLER_AXIS_INVALID` if no match was found.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC SDL_GameControllerButton SDLCALL SDL_GameControllerGetButtonFromString(const char *str);
+
+/**
+ * Convert from an SDL_GameControllerButton enum to a string.
+ *
+ * The caller should not SDL_free() the returned string.
+ *
+ * \param button an enum value for a given SDL_GameControllerButton.
+ * \returns a string for the given button, or NULL if an invalid button is
+ * specified. The string returned is of the format used by
+ * SDL_GameController mapping strings.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerGetButtonFromString
+ */
+extern DECLSPEC const char* SDLCALL SDL_GameControllerGetStringForButton(SDL_GameControllerButton button);
+
+/**
+ * Get the SDL joystick layer binding for a controller button mapping.
+ *
+ * \param gamecontroller a game controller.
+ * \param button an button enum value (an SDL_GameControllerButton value).
+ * \returns a SDL_GameControllerButtonBind describing the bind. On failure
+ * (like the given Controller button doesn't exist on the device),
+ * its `.bindType` will be `SDL_CONTROLLER_BINDTYPE_NONE`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerGetBindForAxis
+ */
+extern DECLSPEC SDL_GameControllerButtonBind SDLCALL
+SDL_GameControllerGetBindForButton(SDL_GameController *gamecontroller,
+ SDL_GameControllerButton button);
+
+/**
+ * Query whether a game controller has a given button.
+ *
+ * This merely reports whether the controller's mapping defined this button,
+ * as that is all the information SDL has about the physical device.
+ *
+ * \param gamecontroller a game controller.
+ * \param button a button enum value (an SDL_GameControllerButton value).
+ * \returns SDL_TRUE if the controller has this button, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GameControllerHasButton(SDL_GameController *gamecontroller,
+ SDL_GameControllerButton button);
+
+/**
+ * Get the current state of a button on a game controller.
+ *
+ * \param gamecontroller a game controller.
+ * \param button a button index (one of the SDL_GameControllerButton values).
+ * \returns 1 for pressed state or 0 for not pressed state or error; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerGetAxis
+ */
+extern DECLSPEC Uint8 SDLCALL SDL_GameControllerGetButton(SDL_GameController *gamecontroller,
+ SDL_GameControllerButton button);
+
+/**
+ * Get the number of touchpads on a game controller.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerGetNumTouchpads(SDL_GameController *gamecontroller);
+
+/**
+ * Get the number of supported simultaneous fingers on a touchpad on a game
+ * controller.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerGetNumTouchpadFingers(SDL_GameController *gamecontroller, int touchpad);
+
+/**
+ * Get the current state of a finger on a touchpad on a game controller.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerGetTouchpadFinger(SDL_GameController *gamecontroller, int touchpad, int finger, Uint8 *state, float *x, float *y, float *pressure);
+
+/**
+ * Return whether a game controller has a particular sensor.
+ *
+ * \param gamecontroller The controller to query.
+ * \param type The type of sensor to query.
+ * \returns SDL_TRUE if the sensor exists, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GameControllerHasSensor(SDL_GameController *gamecontroller, SDL_SensorType type);
+
+/**
+ * Set whether data reporting for a game controller sensor is enabled.
+ *
+ * \param gamecontroller The controller to update.
+ * \param type The type of sensor to enable/disable.
+ * \param enabled Whether data reporting should be enabled.
+ * \returns 0 or -1 if an error occurred.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerSetSensorEnabled(SDL_GameController *gamecontroller, SDL_SensorType type, SDL_bool enabled);
+
+/**
+ * Query whether sensor data reporting is enabled for a game controller.
+ *
+ * \param gamecontroller The controller to query.
+ * \param type The type of sensor to query.
+ * \returns SDL_TRUE if the sensor is enabled, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GameControllerIsSensorEnabled(SDL_GameController *gamecontroller, SDL_SensorType type);
+
+/**
+ * Get the data rate (number of events per second) of a game controller
+ * sensor.
+ *
+ * \param gamecontroller The controller to query.
+ * \param type The type of sensor to query.
+ * \return the data rate, or 0.0f if the data rate is not available.
+ *
+ * \since This function is available since SDL 2.0.16.
+ */
+extern DECLSPEC float SDLCALL SDL_GameControllerGetSensorDataRate(SDL_GameController *gamecontroller, SDL_SensorType type);
+
+/**
+ * Get the current state of a game controller sensor.
+ *
+ * The number of values and interpretation of the data is sensor dependent.
+ * See SDL_sensor.h for the details for each type of sensor.
+ *
+ * \param gamecontroller The controller to query.
+ * \param type The type of sensor to query.
+ * \param data A pointer filled with the current sensor state.
+ * \param num_values The number of values to write to data.
+ * \return 0 or -1 if an error occurred.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerGetSensorData(SDL_GameController *gamecontroller, SDL_SensorType type, float *data, int num_values);
+
+/**
+ * Get the current state of a game controller sensor with the timestamp of the
+ * last update.
+ *
+ * The number of values and interpretation of the data is sensor dependent.
+ * See SDL_sensor.h for the details for each type of sensor.
+ *
+ * \param gamecontroller The controller to query.
+ * \param type The type of sensor to query.
+ * \param timestamp A pointer filled with the timestamp in microseconds of the
+ * current sensor reading if available, or 0 if not.
+ * \param data A pointer filled with the current sensor state.
+ * \param num_values The number of values to write to data.
+ * \return 0 or -1 if an error occurred.
+ *
+ * \since This function is available since SDL 2.26.0.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerGetSensorDataWithTimestamp(SDL_GameController *gamecontroller, SDL_SensorType type, Uint64 *timestamp, float *data, int num_values);
+
+/**
+ * Start a rumble effect on a game controller.
+ *
+ * Each call to this function cancels any previous rumble effect, and calling
+ * it with 0 intensity stops any rumbling.
+ *
+ * \param gamecontroller The controller to vibrate.
+ * \param low_frequency_rumble The intensity of the low frequency (left)
+ * rumble motor, from 0 to 0xFFFF.
+ * \param high_frequency_rumble The intensity of the high frequency (right)
+ * rumble motor, from 0 to 0xFFFF.
+ * \param duration_ms The duration of the rumble effect, in milliseconds.
+ * \returns 0, or -1 if rumble isn't supported on this controller.
+ *
+ * \since This function is available since SDL 2.0.9.
+ *
+ * \sa SDL_GameControllerHasRumble
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerRumble(SDL_GameController *gamecontroller, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble, Uint32 duration_ms);
+
+/**
+ * Start a rumble effect in the game controller's triggers.
+ *
+ * Each call to this function cancels any previous trigger rumble effect, and
+ * calling it with 0 intensity stops any rumbling.
+ *
+ * Note that this is rumbling of the _triggers_ and not the game controller as
+ * a whole. This is currently only supported on Xbox One controllers. If you
+ * want the (more common) whole-controller rumble, use
+ * SDL_GameControllerRumble() instead.
+ *
+ * \param gamecontroller The controller to vibrate.
+ * \param left_rumble The intensity of the left trigger rumble motor, from 0
+ * to 0xFFFF.
+ * \param right_rumble The intensity of the right trigger rumble motor, from 0
+ * to 0xFFFF.
+ * \param duration_ms The duration of the rumble effect, in milliseconds.
+ * \returns 0, or -1 if trigger rumble isn't supported on this controller.
+ *
+ * \since This function is available since SDL 2.0.14.
+ *
+ * \sa SDL_GameControllerHasRumbleTriggers
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerRumbleTriggers(SDL_GameController *gamecontroller, Uint16 left_rumble, Uint16 right_rumble, Uint32 duration_ms);
+
+/**
+ * Query whether a game controller has an LED.
+ *
+ * \param gamecontroller The controller to query.
+ * \returns SDL_TRUE, or SDL_FALSE if this controller does not have a
+ * modifiable LED.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GameControllerHasLED(SDL_GameController *gamecontroller);
+
+/**
+ * Query whether a game controller has rumble support.
+ *
+ * \param gamecontroller The controller to query.
+ * \returns SDL_TRUE, or SDL_FALSE if this controller does not have rumble
+ * support.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_GameControllerRumble
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GameControllerHasRumble(SDL_GameController *gamecontroller);
+
+/**
+ * Query whether a game controller has rumble support on triggers.
+ *
+ * \param gamecontroller The controller to query.
+ * \returns SDL_TRUE, or SDL_FALSE if this controller does not have trigger
+ * rumble support.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_GameControllerRumbleTriggers
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GameControllerHasRumbleTriggers(SDL_GameController *gamecontroller);
+
+/**
+ * Update a game controller's LED color.
+ *
+ * \param gamecontroller The controller to update.
+ * \param red The intensity of the red LED.
+ * \param green The intensity of the green LED.
+ * \param blue The intensity of the blue LED.
+ * \returns 0, or -1 if this controller does not have a modifiable LED.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerSetLED(SDL_GameController *gamecontroller, Uint8 red, Uint8 green, Uint8 blue);
+
+/**
+ * Send a controller specific effect packet
+ *
+ * \param gamecontroller The controller to affect.
+ * \param data The data to send to the controller.
+ * \param size The size of the data to send to the controller.
+ * \returns 0, or -1 if this controller or driver doesn't support effect
+ * packets.
+ *
+ * \since This function is available since SDL 2.0.16.
+ */
+extern DECLSPEC int SDLCALL SDL_GameControllerSendEffect(SDL_GameController *gamecontroller, const void *data, int size);
+
+/**
+ * Close a game controller previously opened with SDL_GameControllerOpen().
+ *
+ * \param gamecontroller a game controller identifier previously returned by
+ * SDL_GameControllerOpen().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerOpen
+ */
+extern DECLSPEC void SDLCALL SDL_GameControllerClose(SDL_GameController *gamecontroller);
+
+/**
+ * Return the sfSymbolsName for a given button on a game controller on Apple
+ * platforms.
+ *
+ * \param gamecontroller the controller to query.
+ * \param button a button on the game controller.
+ * \returns the sfSymbolsName or NULL if the name can't be found.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_GameControllerGetAppleSFSymbolsNameForAxis
+ */
+extern DECLSPEC const char* SDLCALL SDL_GameControllerGetAppleSFSymbolsNameForButton(SDL_GameController *gamecontroller, SDL_GameControllerButton button);
+
+/**
+ * Return the sfSymbolsName for a given axis on a game controller on Apple
+ * platforms.
+ *
+ * \param gamecontroller the controller to query.
+ * \param axis an axis on the game controller.
+ * \returns the sfSymbolsName or NULL if the name can't be found.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_GameControllerGetAppleSFSymbolsNameForButton
+ */
+extern DECLSPEC const char* SDLCALL SDL_GameControllerGetAppleSFSymbolsNameForAxis(SDL_GameController *gamecontroller, SDL_GameControllerAxis axis);
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_gamecontroller_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_gesture.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_gesture.h
new file mode 100644
index 00000000..acfa56f3
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_gesture.h
@@ -0,0 +1,117 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryGesture
+ *
+ * Include file for SDL gesture event handling.
+ */
+
+#ifndef SDL_gesture_h_
+#define SDL_gesture_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+#include "SDL_video.h"
+
+#include "SDL_touch.h"
+
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef Sint64 SDL_GestureID;
+
+/* Function prototypes */
+
+/**
+ * Begin recording a gesture on a specified touch device or all touch devices.
+ *
+ * If the parameter `touchId` is -1 (i.e., all devices), this function will
+ * always return 1, regardless of whether there actually are any devices.
+ *
+ * \param touchId the touch device id, or -1 for all touch devices.
+ * \returns 1 on success or 0 if the specified device could not be found.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetTouchDevice
+ */
+extern DECLSPEC int SDLCALL SDL_RecordGesture(SDL_TouchID touchId);
+
+
+/**
+ * Save all currently loaded Dollar Gesture templates.
+ *
+ * \param dst a SDL_RWops to save to.
+ * \returns the number of saved templates on success or 0 on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LoadDollarTemplates
+ * \sa SDL_SaveDollarTemplate
+ */
+extern DECLSPEC int SDLCALL SDL_SaveAllDollarTemplates(SDL_RWops *dst);
+
+/**
+ * Save a currently loaded Dollar Gesture template.
+ *
+ * \param gestureId a gesture id.
+ * \param dst a SDL_RWops to save to.
+ * \returns 1 on success or 0 on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LoadDollarTemplates
+ * \sa SDL_SaveAllDollarTemplates
+ */
+extern DECLSPEC int SDLCALL SDL_SaveDollarTemplate(SDL_GestureID gestureId,SDL_RWops *dst);
+
+
+/**
+ * Load Dollar Gesture templates from a file.
+ *
+ * \param touchId a touch id.
+ * \param src a SDL_RWops to load from.
+ * \returns the number of loaded templates on success or a negative error code
+ * (or 0) on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_SaveAllDollarTemplates
+ * \sa SDL_SaveDollarTemplate
+ */
+extern DECLSPEC int SDLCALL SDL_LoadDollarTemplates(SDL_TouchID touchId, SDL_RWops *src);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_gesture_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_guid.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_guid.h
new file mode 100644
index 00000000..fd9a50e3
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_guid.h
@@ -0,0 +1,107 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* WIKI CATEGORY: GUID */
+
+/**
+ * # CategoryGUID
+ *
+ * A GUID is a 128-bit value that represents something that is uniquely
+ * identifiable by this value: "globally unique."
+ */
+
+#ifndef SDL_guid_h_
+#define SDL_guid_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * An SDL_GUID is a 128-bit identifier.
+ *
+ * This is an acronym for "Globally Unique ID."
+ *
+ * While a GUID can be used to assign a unique value to almost anything, in
+ * SDL these are largely used to identify input devices across runs of SDL
+ * programs on the same platform.If the device is detached and then
+ * re-attached to a different port, or if the base system is rebooted, the
+ * device should still report the same GUID.
+ *
+ * GUIDs are as precise as possible but are not guaranteed to distinguish
+ * physically distinct but equivalent devices. For example, two game
+ * controllers from the same vendor with the same product ID and revision may
+ * have the same GUID.
+ *
+ * GUIDs may be platform-dependent (i.e., the same device may report different
+ * GUIDs on different operating systems).
+ */
+typedef struct SDL_GUID {
+ Uint8 data[16];
+} SDL_GUID;
+
+/* Function prototypes */
+
+/**
+ * Get an ASCII string representation for a given SDL_GUID.
+ *
+ * You should supply at least 33 bytes for pszGUID.
+ *
+ * \param guid the SDL_GUID you wish to convert to string.
+ * \param pszGUID buffer in which to write the ASCII string.
+ * \param cbGUID the size of pszGUID.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GUIDFromString
+ */
+extern DECLSPEC void SDLCALL SDL_GUIDToString(SDL_GUID guid, char *pszGUID, int cbGUID);
+
+/**
+ * Convert a GUID string into a SDL_GUID structure.
+ *
+ * Performs no error checking. If this function is given a string containing
+ * an invalid GUID, the function will silently succeed, but the GUID generated
+ * will not be useful.
+ *
+ * \param pchGUID string containing an ASCII representation of a GUID.
+ * \returns a SDL_GUID structure.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GUIDToString
+ */
+extern DECLSPEC SDL_GUID SDLCALL SDL_GUIDFromString(const char *pchGUID);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_guid_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_haptic.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_haptic.h
new file mode 100644
index 00000000..f679c573
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_haptic.h
@@ -0,0 +1,1354 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryHaptic
+ *
+ * SDL haptic subsystem allows you to control haptic (force feedback) devices.
+ *
+ * The basic usage is as follows:
+ *
+ * - Initialize the subsystem (SDL_INIT_HAPTIC).
+ * - Open a haptic device.
+ * - SDL_HapticOpen() to open from index.
+ * - SDL_HapticOpenFromJoystick() to open from an existing joystick.
+ * - Create an effect (SDL_HapticEffect).
+ * - Upload the effect with SDL_HapticNewEffect().
+ * - Run the effect with SDL_HapticRunEffect().
+ * - (optional) Free the effect with SDL_HapticDestroyEffect().
+ * - Close the haptic device with SDL_HapticClose().
+ *
+ * Simple rumble example:
+ *
+ * ```c
+ * SDL_Haptic *haptic;
+ *
+ * // Open the device
+ * haptic = SDL_HapticOpen( 0 );
+ * if (haptic == NULL)
+ * return -1;
+ *
+ * // Initialize simple rumble
+ * if (SDL_HapticRumbleInit( haptic ) != 0)
+ * return -1;
+ *
+ * // Play effect at 50% strength for 2 seconds
+ * if (SDL_HapticRumblePlay( haptic, 0.5, 2000 ) != 0)
+ * return -1;
+ * SDL_Delay( 2000 );
+ *
+ * // Clean up
+ * SDL_HapticClose( haptic );
+ * ```
+ *
+ * Complete example:
+ *
+ * ```c
+ * int test_haptic( SDL_Joystick * joystick ) {
+ * SDL_Haptic *haptic;
+ * SDL_HapticEffect effect;
+ * int effect_id;
+ *
+ * // Open the device
+ * haptic = SDL_HapticOpenFromJoystick( joystick );
+ * if (haptic == NULL) return -1; // Most likely joystick isn't haptic
+ *
+ * // See if it can do sine waves
+ * if ((SDL_HapticQuery(haptic) & SDL_HAPTIC_SINE)==0) {
+ * SDL_HapticClose(haptic); // No sine effect
+ * return -1;
+ * }
+ *
+ * // Create the effect
+ * SDL_memset( &effect, 0, sizeof(SDL_HapticEffect) ); // 0 is safe default
+ * effect.type = SDL_HAPTIC_SINE;
+ * effect.periodic.direction.type = SDL_HAPTIC_POLAR; // Polar coordinates
+ * effect.periodic.direction.dir[0] = 18000; // Force comes from south
+ * effect.periodic.period = 1000; // 1000 ms
+ * effect.periodic.magnitude = 20000; // 20000/32767 strength
+ * effect.periodic.length = 5000; // 5 seconds long
+ * effect.periodic.attack_length = 1000; // Takes 1 second to get max strength
+ * effect.periodic.fade_length = 1000; // Takes 1 second to fade away
+ *
+ * // Upload the effect
+ * effect_id = SDL_HapticNewEffect( haptic, &effect );
+ *
+ * // Test the effect
+ * SDL_HapticRunEffect( haptic, effect_id, 1 );
+ * SDL_Delay( 5000); // Wait for the effect to finish
+ *
+ * // We destroy the effect, although closing the device also does this
+ * SDL_HapticDestroyEffect( haptic, effect_id );
+ *
+ * // Close the device
+ * SDL_HapticClose(haptic);
+ *
+ * return 0; // Success
+ * }
+ * ```
+ */
+
+#ifndef SDL_haptic_h_
+#define SDL_haptic_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+#include "SDL_joystick.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+/* FIXME: For SDL 2.1, adjust all the magnitude variables to be Uint16 (0xFFFF).
+ *
+ * At the moment the magnitude variables are mixed between signed/unsigned, and
+ * it is also not made clear that ALL of those variables expect a max of 0x7FFF.
+ *
+ * Some platforms may have higher precision than that (Linux FF, Windows XInput)
+ * so we should fix the inconsistency in favor of higher possible precision,
+ * adjusting for platforms that use different scales.
+ * -flibit
+ */
+
+/**
+ * \typedef SDL_Haptic
+ *
+ * \brief The haptic structure used to identify an SDL haptic.
+ *
+ * \sa SDL_HapticOpen
+ * \sa SDL_HapticOpenFromJoystick
+ * \sa SDL_HapticClose
+ */
+struct _SDL_Haptic;
+typedef struct _SDL_Haptic SDL_Haptic;
+
+
+/**
+ * \name Haptic features
+ *
+ * Different haptic features a device can have.
+ */
+/* @{ */
+
+/**
+ * \name Haptic effects
+ */
+/* @{ */
+
+/**
+ * Constant effect supported.
+ *
+ * Constant haptic effect.
+ *
+ * \sa SDL_HapticCondition
+ */
+#define SDL_HAPTIC_CONSTANT (1u<<0)
+
+/**
+ * Sine wave effect supported.
+ *
+ * Periodic haptic effect that simulates sine waves.
+ *
+ * \sa SDL_HapticPeriodic
+ */
+#define SDL_HAPTIC_SINE (1u<<1)
+
+/**
+ * Left/Right effect supported.
+ *
+ * Haptic effect for direct control over high/low frequency motors.
+ *
+ * \sa SDL_HapticLeftRight
+ */
+#define SDL_HAPTIC_LEFTRIGHT (1u<<2)
+
+/* !!! FIXME: put this back when we have more bits in 2.1 */
+/* #define SDL_HAPTIC_SQUARE (1<<2) */
+
+/**
+ * Triangle wave effect supported.
+ *
+ * Periodic haptic effect that simulates triangular waves.
+ *
+ * \sa SDL_HapticPeriodic
+ */
+#define SDL_HAPTIC_TRIANGLE (1u<<3)
+
+/**
+ * Sawtoothup wave effect supported.
+ *
+ * Periodic haptic effect that simulates saw tooth up waves.
+ *
+ * \sa SDL_HapticPeriodic
+ */
+#define SDL_HAPTIC_SAWTOOTHUP (1u<<4)
+
+/**
+ * Sawtoothdown wave effect supported.
+ *
+ * Periodic haptic effect that simulates saw tooth down waves.
+ *
+ * \sa SDL_HapticPeriodic
+ */
+#define SDL_HAPTIC_SAWTOOTHDOWN (1u<<5)
+
+/**
+ * Ramp effect supported.
+ *
+ * Ramp haptic effect.
+ *
+ * \sa SDL_HapticRamp
+ */
+#define SDL_HAPTIC_RAMP (1u<<6)
+
+/**
+ * Spring effect supported - uses axes position.
+ *
+ * Condition haptic effect that simulates a spring. Effect is based on the
+ * axes position.
+ *
+ * \sa SDL_HapticCondition
+ */
+#define SDL_HAPTIC_SPRING (1u<<7)
+
+/**
+ * Damper effect supported - uses axes velocity.
+ *
+ * Condition haptic effect that simulates dampening. Effect is based on the
+ * axes velocity.
+ *
+ * \sa SDL_HapticCondition
+ */
+#define SDL_HAPTIC_DAMPER (1u<<8)
+
+/**
+ * Inertia effect supported - uses axes acceleration.
+ *
+ * Condition haptic effect that simulates inertia. Effect is based on the axes
+ * acceleration.
+ *
+ * \sa SDL_HapticCondition
+ */
+#define SDL_HAPTIC_INERTIA (1u<<9)
+
+/**
+ * Friction effect supported - uses axes movement.
+ *
+ * Condition haptic effect that simulates friction. Effect is based on the
+ * axes movement.
+ *
+ * \sa SDL_HapticCondition
+ */
+#define SDL_HAPTIC_FRICTION (1u<<10)
+
+/**
+ * Custom effect is supported.
+ *
+ * User defined custom haptic effect.
+ */
+#define SDL_HAPTIC_CUSTOM (1u<<11)
+
+/* @} *//* Haptic effects */
+
+/* These last few are features the device has, not effects */
+
+/**
+ * Device can set global gain.
+ *
+ * Device supports setting the global gain.
+ *
+ * \sa SDL_HapticSetGain
+ */
+#define SDL_HAPTIC_GAIN (1u<<12)
+
+/**
+ * Device can set autocenter.
+ *
+ * Device supports setting autocenter.
+ *
+ * \sa SDL_HapticSetAutocenter
+ */
+#define SDL_HAPTIC_AUTOCENTER (1u<<13)
+
+/**
+ * Device can be queried for effect status.
+ *
+ * Device supports querying effect status.
+ *
+ * \sa SDL_HapticGetEffectStatus
+ */
+#define SDL_HAPTIC_STATUS (1u<<14)
+
+/**
+ * Device can be paused.
+ *
+ * Devices supports being paused.
+ *
+ * \sa SDL_HapticPause
+ * \sa SDL_HapticUnpause
+ */
+#define SDL_HAPTIC_PAUSE (1u<<15)
+
+
+/**
+ * \name Direction encodings
+ */
+/* @{ */
+
+/**
+ * Uses polar coordinates for the direction.
+ *
+ * \sa SDL_HapticDirection
+ */
+#define SDL_HAPTIC_POLAR 0
+
+/**
+ * Uses cartesian coordinates for the direction.
+ *
+ * \sa SDL_HapticDirection
+ */
+#define SDL_HAPTIC_CARTESIAN 1
+
+/**
+ * Uses spherical coordinates for the direction.
+ *
+ * \sa SDL_HapticDirection
+ */
+#define SDL_HAPTIC_SPHERICAL 2
+
+/**
+ * Use this value to play an effect on the steering wheel axis.
+ *
+ * This provides better compatibility across platforms and devices as SDL will
+ * guess the correct axis.
+ *
+ * \sa SDL_HapticDirection
+ */
+#define SDL_HAPTIC_STEERING_AXIS 3
+
+/* @} *//* Direction encodings */
+
+/* @} *//* Haptic features */
+
+/*
+ * Misc defines.
+ */
+
+/**
+ * Used to play a device an infinite number of times.
+ *
+ * \sa SDL_HapticRunEffect
+ */
+#define SDL_HAPTIC_INFINITY 4294967295U
+
+
+/**
+ * Structure that represents a haptic direction.
+ *
+ * This is the direction where the force comes from, instead of the direction
+ * in which the force is exerted.
+ *
+ * Directions can be specified by:
+ *
+ * - SDL_HAPTIC_POLAR : Specified by polar coordinates.
+ * - SDL_HAPTIC_CARTESIAN : Specified by cartesian coordinates.
+ * - SDL_HAPTIC_SPHERICAL : Specified by spherical coordinates.
+ *
+ * Cardinal directions of the haptic device are relative to the positioning of
+ * the device. North is considered to be away from the user.
+ *
+ * The following diagram represents the cardinal directions:
+ *
+ * ```
+ * .--.
+ * |__| .-------.
+ * |=.| |.-----.|
+ * |--| || ||
+ * | | |'-----'|
+ * |__|~')_____('
+ * [ COMPUTER ]
+ *
+ *
+ * North (0,-1)
+ * ^
+ * |
+ * |
+ * (-1,0) West <----[ HAPTIC ]----> East (1,0)
+ * |
+ * |
+ * v
+ * South (0,1)
+ *
+ *
+ * [ USER ]
+ * \|||/
+ * (o o)
+ * ---ooO-(_)-Ooo---
+ * ```
+ *
+ * If type is SDL_HAPTIC_POLAR, direction is encoded by hundredths of a degree
+ * starting north and turning clockwise. SDL_HAPTIC_POLAR only uses the first
+ * `dir` parameter. The cardinal directions would be:
+ *
+ * - North: 0 (0 degrees)
+ * - East: 9000 (90 degrees)
+ * - South: 18000 (180 degrees)
+ * - West: 27000 (270 degrees)
+ *
+ * If type is SDL_HAPTIC_CARTESIAN, direction is encoded by three positions (X
+ * axis, Y axis and Z axis (with 3 axes)). SDL_HAPTIC_CARTESIAN uses the first
+ * three `dir` parameters. The cardinal directions would be:
+ *
+ * - North: 0,-1, 0
+ * - East: 1, 0, 0
+ * - South: 0, 1, 0
+ * - West: -1, 0, 0
+ *
+ * The Z axis represents the height of the effect if supported, otherwise it's
+ * unused. In cartesian encoding (1, 2) would be the same as (2, 4), you can
+ * use any multiple you want, only the direction matters.
+ *
+ * If type is SDL_HAPTIC_SPHERICAL, direction is encoded by two rotations. The
+ * first two `dir` parameters are used. The `dir` parameters are as follows
+ * (all values are in hundredths of degrees):
+ *
+ * - Degrees from (1, 0) rotated towards (0, 1).
+ * - Degrees towards (0, 0, 1) (device needs at least 3 axes).
+ *
+ * Example of force coming from the south with all encodings (force coming
+ * from the south means the user will have to pull the stick to counteract):
+ *
+ * ```c
+ * SDL_HapticDirection direction;
+ *
+ * // Cartesian directions
+ * direction.type = SDL_HAPTIC_CARTESIAN; // Using cartesian direction encoding.
+ * direction.dir[0] = 0; // X position
+ * direction.dir[1] = 1; // Y position
+ * // Assuming the device has 2 axes, we don't need to specify third parameter.
+ *
+ * // Polar directions
+ * direction.type = SDL_HAPTIC_POLAR; // We'll be using polar direction encoding.
+ * direction.dir[0] = 18000; // Polar only uses first parameter
+ *
+ * // Spherical coordinates
+ * direction.type = SDL_HAPTIC_SPHERICAL; // Spherical encoding
+ * direction.dir[0] = 9000; // Since we only have two axes we don't need more parameters.
+ * ```
+ *
+ * \sa SDL_HAPTIC_POLAR
+ * \sa SDL_HAPTIC_CARTESIAN
+ * \sa SDL_HAPTIC_SPHERICAL
+ * \sa SDL_HAPTIC_STEERING_AXIS
+ * \sa SDL_HapticEffect
+ * \sa SDL_HapticNumAxes
+ */
+typedef struct SDL_HapticDirection
+{
+ Uint8 type; /**< The type of encoding. */
+ Sint32 dir[3]; /**< The encoded direction. */
+} SDL_HapticDirection;
+
+
+/**
+ * A structure containing a template for a Constant effect.
+ *
+ * This struct is exclusively for the SDL_HAPTIC_CONSTANT effect.
+ *
+ * A constant effect applies a constant force in the specified direction to
+ * the joystick.
+ *
+ * \sa SDL_HAPTIC_CONSTANT
+ * \sa SDL_HapticEffect
+ */
+typedef struct SDL_HapticConstant
+{
+ /* Header */
+ Uint16 type; /**< SDL_HAPTIC_CONSTANT */
+ SDL_HapticDirection direction; /**< Direction of the effect. */
+
+ /* Replay */
+ Uint32 length; /**< Duration of the effect. */
+ Uint16 delay; /**< Delay before starting the effect. */
+
+ /* Trigger */
+ Uint16 button; /**< Button that triggers the effect. */
+ Uint16 interval; /**< How soon it can be triggered again after button. */
+
+ /* Constant */
+ Sint16 level; /**< Strength of the constant effect. */
+
+ /* Envelope */
+ Uint16 attack_length; /**< Duration of the attack. */
+ Uint16 attack_level; /**< Level at the start of the attack. */
+ Uint16 fade_length; /**< Duration of the fade. */
+ Uint16 fade_level; /**< Level at the end of the fade. */
+} SDL_HapticConstant;
+
+/**
+ * A structure containing a template for a Periodic effect.
+ *
+ * The struct handles the following effects:
+ *
+ * - SDL_HAPTIC_SINE
+ * - SDL_HAPTIC_SQUARE
+ * - SDL_HAPTIC_TRIANGLE
+ * - SDL_HAPTIC_SAWTOOTHUP
+ * - SDL_HAPTIC_SAWTOOTHDOWN
+ *
+ * A periodic effect consists in a wave-shaped effect that repeats itself over
+ * time. The type determines the shape of the wave and the parameters
+ * determine the dimensions of the wave.
+ *
+ * Phase is given by hundredth of a degree meaning that giving the phase a
+ * value of 9000 will displace it 25% of its period. Here are sample values:
+ *
+ * - 0: No phase displacement.
+ * - 9000: Displaced 25% of its period.
+ * - 18000: Displaced 50% of its period.
+ * - 27000: Displaced 75% of its period.
+ * - 36000: Displaced 100% of its period, same as 0, but 0 is preferred.
+ *
+ * Examples:
+ *
+ * ```
+ * SDL_HAPTIC_SINE
+ * __ __ __ __
+ * / \ / \ / \ /
+ * / \__/ \__/ \__/
+ *
+ * SDL_HAPTIC_SQUARE
+ * __ __ __ __ __
+ * | | | | | | | | | |
+ * | |__| |__| |__| |__| |
+ *
+ * SDL_HAPTIC_TRIANGLE
+ * /\ /\ /\ /\ /\
+ * / \ / \ / \ / \ /
+ * / \/ \/ \/ \/
+ *
+ * SDL_HAPTIC_SAWTOOTHUP
+ * /| /| /| /| /| /| /|
+ * / | / | / | / | / | / | / |
+ * / |/ |/ |/ |/ |/ |/ |
+ *
+ * SDL_HAPTIC_SAWTOOTHDOWN
+ * \ |\ |\ |\ |\ |\ |\ |
+ * \ | \ | \ | \ | \ | \ | \ |
+ * \| \| \| \| \| \| \|
+ * ```
+ *
+ * \sa SDL_HAPTIC_SINE
+ * \sa SDL_HAPTIC_LEFTRIGHT
+ * \sa SDL_HAPTIC_TRIANGLE
+ * \sa SDL_HAPTIC_SAWTOOTHUP
+ * \sa SDL_HAPTIC_SAWTOOTHDOWN
+ * \sa SDL_HapticEffect
+ */
+typedef struct SDL_HapticPeriodic
+{
+ /* Header */
+ Uint16 type; /**< SDL_HAPTIC_SINE, SDL_HAPTIC_LEFTRIGHT,
+ SDL_HAPTIC_TRIANGLE, SDL_HAPTIC_SAWTOOTHUP or
+ SDL_HAPTIC_SAWTOOTHDOWN */
+ SDL_HapticDirection direction; /**< Direction of the effect. */
+
+ /* Replay */
+ Uint32 length; /**< Duration of the effect. */
+ Uint16 delay; /**< Delay before starting the effect. */
+
+ /* Trigger */
+ Uint16 button; /**< Button that triggers the effect. */
+ Uint16 interval; /**< How soon it can be triggered again after button. */
+
+ /* Periodic */
+ Uint16 period; /**< Period of the wave. */
+ Sint16 magnitude; /**< Peak value; if negative, equivalent to 180 degrees extra phase shift. */
+ Sint16 offset; /**< Mean value of the wave. */
+ Uint16 phase; /**< Positive phase shift given by hundredth of a degree. */
+
+ /* Envelope */
+ Uint16 attack_length; /**< Duration of the attack. */
+ Uint16 attack_level; /**< Level at the start of the attack. */
+ Uint16 fade_length; /**< Duration of the fade. */
+ Uint16 fade_level; /**< Level at the end of the fade. */
+} SDL_HapticPeriodic;
+
+/**
+ * A structure containing a template for a Condition effect.
+ *
+ * The struct handles the following effects:
+ *
+ * - SDL_HAPTIC_SPRING: Effect based on axes position.
+ * - SDL_HAPTIC_DAMPER: Effect based on axes velocity.
+ * - SDL_HAPTIC_INERTIA: Effect based on axes acceleration.
+ * - SDL_HAPTIC_FRICTION: Effect based on axes movement.
+ *
+ * Direction is handled by condition internals instead of a direction member.
+ * The condition effect specific members have three parameters. The first
+ * refers to the X axis, the second refers to the Y axis and the third refers
+ * to the Z axis. The right terms refer to the positive side of the axis and
+ * the left terms refer to the negative side of the axis. Please refer to the
+ * SDL_HapticDirection diagram for which side is positive and which is
+ * negative.
+ *
+ * \sa SDL_HapticDirection
+ * \sa SDL_HAPTIC_SPRING
+ * \sa SDL_HAPTIC_DAMPER
+ * \sa SDL_HAPTIC_INERTIA
+ * \sa SDL_HAPTIC_FRICTION
+ * \sa SDL_HapticEffect
+ */
+typedef struct SDL_HapticCondition
+{
+ /* Header */
+ Uint16 type; /**< SDL_HAPTIC_SPRING, SDL_HAPTIC_DAMPER,
+ SDL_HAPTIC_INERTIA or SDL_HAPTIC_FRICTION */
+ SDL_HapticDirection direction; /**< Direction of the effect. */
+
+ /* Replay */
+ Uint32 length; /**< Duration of the effect. */
+ Uint16 delay; /**< Delay before starting the effect. */
+
+ /* Trigger */
+ Uint16 button; /**< Button that triggers the effect. */
+ Uint16 interval; /**< How soon it can be triggered again after button. */
+
+ /* Condition */
+ Uint16 right_sat[3]; /**< Level when joystick is to the positive side; max 0xFFFF. */
+ Uint16 left_sat[3]; /**< Level when joystick is to the negative side; max 0xFFFF. */
+ Sint16 right_coeff[3]; /**< How fast to increase the force towards the positive side. */
+ Sint16 left_coeff[3]; /**< How fast to increase the force towards the negative side. */
+ Uint16 deadband[3]; /**< Size of the dead zone; max 0xFFFF: whole axis-range when 0-centered. */
+ Sint16 center[3]; /**< Position of the dead zone. */
+} SDL_HapticCondition;
+
+/**
+ * A structure containing a template for a Ramp effect.
+ *
+ * This struct is exclusively for the SDL_HAPTIC_RAMP effect.
+ *
+ * The ramp effect starts at start strength and ends at end strength. It
+ * augments in linear fashion. If you use attack and fade with a ramp the
+ * effects get added to the ramp effect making the effect become quadratic
+ * instead of linear.
+ *
+ * \sa SDL_HAPTIC_RAMP
+ * \sa SDL_HapticEffect
+ */
+typedef struct SDL_HapticRamp
+{
+ /* Header */
+ Uint16 type; /**< SDL_HAPTIC_RAMP */
+ SDL_HapticDirection direction; /**< Direction of the effect. */
+
+ /* Replay */
+ Uint32 length; /**< Duration of the effect. */
+ Uint16 delay; /**< Delay before starting the effect. */
+
+ /* Trigger */
+ Uint16 button; /**< Button that triggers the effect. */
+ Uint16 interval; /**< How soon it can be triggered again after button. */
+
+ /* Ramp */
+ Sint16 start; /**< Beginning strength level. */
+ Sint16 end; /**< Ending strength level. */
+
+ /* Envelope */
+ Uint16 attack_length; /**< Duration of the attack. */
+ Uint16 attack_level; /**< Level at the start of the attack. */
+ Uint16 fade_length; /**< Duration of the fade. */
+ Uint16 fade_level; /**< Level at the end of the fade. */
+} SDL_HapticRamp;
+
+/**
+ * A structure containing a template for a Left/Right effect.
+ *
+ * This struct is exclusively for the SDL_HAPTIC_LEFTRIGHT effect.
+ *
+ * The Left/Right effect is used to explicitly control the large and small
+ * motors, commonly found in modern game controllers. The small (right) motor
+ * is high frequency, and the large (left) motor is low frequency.
+ *
+ * \sa SDL_HAPTIC_LEFTRIGHT
+ * \sa SDL_HapticEffect
+ */
+typedef struct SDL_HapticLeftRight
+{
+ /* Header */
+ Uint16 type; /**< SDL_HAPTIC_LEFTRIGHT */
+
+ /* Replay */
+ Uint32 length; /**< Duration of the effect in milliseconds. */
+
+ /* Rumble */
+ Uint16 large_magnitude; /**< Control of the large controller motor. */
+ Uint16 small_magnitude; /**< Control of the small controller motor. */
+} SDL_HapticLeftRight;
+
+/**
+ * A structure containing a template for the SDL_HAPTIC_CUSTOM effect.
+ *
+ * This struct is exclusively for the SDL_HAPTIC_CUSTOM effect.
+ *
+ * A custom force feedback effect is much like a periodic effect, where the
+ * application can define its exact shape. You will have to allocate the data
+ * yourself. Data should consist of channels * samples Uint16 samples.
+ *
+ * If channels is one, the effect is rotated using the defined direction.
+ * Otherwise it uses the samples in data for the different axes.
+ *
+ * \sa SDL_HAPTIC_CUSTOM
+ * \sa SDL_HapticEffect
+ */
+typedef struct SDL_HapticCustom
+{
+ /* Header */
+ Uint16 type; /**< SDL_HAPTIC_CUSTOM */
+ SDL_HapticDirection direction; /**< Direction of the effect. */
+
+ /* Replay */
+ Uint32 length; /**< Duration of the effect. */
+ Uint16 delay; /**< Delay before starting the effect. */
+
+ /* Trigger */
+ Uint16 button; /**< Button that triggers the effect. */
+ Uint16 interval; /**< How soon it can be triggered again after button. */
+
+ /* Custom */
+ Uint8 channels; /**< Axes to use, minimum of one. */
+ Uint16 period; /**< Sample periods. */
+ Uint16 samples; /**< Amount of samples. */
+ Uint16 *data; /**< Should contain channels*samples items. */
+
+ /* Envelope */
+ Uint16 attack_length; /**< Duration of the attack. */
+ Uint16 attack_level; /**< Level at the start of the attack. */
+ Uint16 fade_length; /**< Duration of the fade. */
+ Uint16 fade_level; /**< Level at the end of the fade. */
+} SDL_HapticCustom;
+
+/**
+ * The generic template for any haptic effect.
+ *
+ * All values max at 32767 (0x7FFF). Signed values also can be negative. Time
+ * values unless specified otherwise are in milliseconds.
+ *
+ * You can also pass SDL_HAPTIC_INFINITY to length instead of a 0-32767 value.
+ * Neither delay, interval, attack_length nor fade_length support
+ * SDL_HAPTIC_INFINITY. Fade will also not be used since effect never ends.
+ *
+ * Additionally, the SDL_HAPTIC_RAMP effect does not support a duration of
+ * SDL_HAPTIC_INFINITY.
+ *
+ * Button triggers may not be supported on all devices, it is advised to not
+ * use them if possible. Buttons start at index 1 instead of index 0 like the
+ * joystick.
+ *
+ * If both attack_length and fade_level are 0, the envelope is not used,
+ * otherwise both values are used.
+ *
+ * Common parts:
+ *
+ * ```c
+ * // Replay - All effects have this
+ * Uint32 length; // Duration of effect (ms).
+ * Uint16 delay; // Delay before starting effect.
+ *
+ * // Trigger - All effects have this
+ * Uint16 button; // Button that triggers effect.
+ * Uint16 interval; // How soon before effect can be triggered again.
+ *
+ * // Envelope - All effects except condition effects have this
+ * Uint16 attack_length; // Duration of the attack (ms).
+ * Uint16 attack_level; // Level at the start of the attack.
+ * Uint16 fade_length; // Duration of the fade out (ms).
+ * Uint16 fade_level; // Level at the end of the fade.
+ * ```
+ *
+ * Here we have an example of a constant effect evolution in time:
+ *
+ * ```
+ * Strength
+ * ^
+ * |
+ * | effect level --> _________________
+ * | / \
+ * | / \
+ * | / \
+ * | / \
+ * | attack_level --> | \
+ * | | | <--- fade_level
+ * |
+ * +--------------------------------------------------> Time
+ * [--] [---]
+ * attack_length fade_length
+ *
+ * [------------------][-----------------------]
+ * delay length
+ * ```
+ *
+ * Note either the attack_level or the fade_level may be above the actual
+ * effect level.
+ *
+ * \sa SDL_HapticConstant
+ * \sa SDL_HapticPeriodic
+ * \sa SDL_HapticCondition
+ * \sa SDL_HapticRamp
+ * \sa SDL_HapticLeftRight
+ * \sa SDL_HapticCustom
+ */
+typedef union SDL_HapticEffect
+{
+ /* Common for all force feedback effects */
+ Uint16 type; /**< Effect type. */
+ SDL_HapticConstant constant; /**< Constant effect. */
+ SDL_HapticPeriodic periodic; /**< Periodic effect. */
+ SDL_HapticCondition condition; /**< Condition effect. */
+ SDL_HapticRamp ramp; /**< Ramp effect. */
+ SDL_HapticLeftRight leftright; /**< Left/Right effect. */
+ SDL_HapticCustom custom; /**< Custom effect. */
+} SDL_HapticEffect;
+
+
+/* Function prototypes */
+
+/**
+ * Count the number of haptic devices attached to the system.
+ *
+ * \returns the number of haptic devices detected on the system or a negative
+ * error code on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticName
+ */
+extern DECLSPEC int SDLCALL SDL_NumHaptics(void);
+
+/**
+ * Get the implementation dependent name of a haptic device.
+ *
+ * This can be called before any joysticks are opened. If no name can be
+ * found, this function returns NULL.
+ *
+ * \param device_index index of the device to query.
+ * \returns the name of the device or NULL on failure; call SDL_GetError() for
+ * more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_NumHaptics
+ */
+extern DECLSPEC const char *SDLCALL SDL_HapticName(int device_index);
+
+/**
+ * Open a haptic device for use.
+ *
+ * The index passed as an argument refers to the N'th haptic device on this
+ * system.
+ *
+ * When opening a haptic device, its gain will be set to maximum and
+ * autocenter will be disabled. To modify these values use SDL_HapticSetGain()
+ * and SDL_HapticSetAutocenter().
+ *
+ * \param device_index index of the device to open.
+ * \returns the device identifier or NULL on failure; call SDL_GetError() for
+ * more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticClose
+ * \sa SDL_HapticIndex
+ * \sa SDL_HapticOpenFromJoystick
+ * \sa SDL_HapticOpenFromMouse
+ * \sa SDL_HapticPause
+ * \sa SDL_HapticSetAutocenter
+ * \sa SDL_HapticSetGain
+ * \sa SDL_HapticStopAll
+ */
+extern DECLSPEC SDL_Haptic *SDLCALL SDL_HapticOpen(int device_index);
+
+/**
+ * Check if the haptic device at the designated index has been opened.
+ *
+ * \param device_index the index of the device to query.
+ * \returns 1 if it has been opened, 0 if it hasn't or on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticIndex
+ * \sa SDL_HapticOpen
+ */
+extern DECLSPEC int SDLCALL SDL_HapticOpened(int device_index);
+
+/**
+ * Get the index of a haptic device.
+ *
+ * \param haptic the SDL_Haptic device to query.
+ * \returns the index of the specified haptic device or a negative error code
+ * on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticOpen
+ * \sa SDL_HapticOpened
+ */
+extern DECLSPEC int SDLCALL SDL_HapticIndex(SDL_Haptic * haptic);
+
+/**
+ * Query whether or not the current mouse has haptic capabilities.
+ *
+ * \returns SDL_TRUE if the mouse is haptic or SDL_FALSE if it isn't.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticOpenFromMouse
+ */
+extern DECLSPEC int SDLCALL SDL_MouseIsHaptic(void);
+
+/**
+ * Try to open a haptic device from the current mouse.
+ *
+ * \returns the haptic device identifier or NULL on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticOpen
+ * \sa SDL_MouseIsHaptic
+ */
+extern DECLSPEC SDL_Haptic *SDLCALL SDL_HapticOpenFromMouse(void);
+
+/**
+ * Query if a joystick has haptic features.
+ *
+ * \param joystick the SDL_Joystick to test for haptic capabilities.
+ * \returns SDL_TRUE if the joystick is haptic, SDL_FALSE if it isn't, or a
+ * negative error code on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticOpenFromJoystick
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickIsHaptic(SDL_Joystick * joystick);
+
+/**
+ * Open a haptic device for use from a joystick device.
+ *
+ * You must still close the haptic device separately. It will not be closed
+ * with the joystick.
+ *
+ * When opened from a joystick you should first close the haptic device before
+ * closing the joystick device. If not, on some implementations the haptic
+ * device will also get unallocated and you'll be unable to use force feedback
+ * on that device.
+ *
+ * \param joystick the SDL_Joystick to create a haptic device from.
+ * \returns a valid haptic device identifier on success or NULL on failure;
+ * call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticClose
+ * \sa SDL_HapticOpen
+ * \sa SDL_JoystickIsHaptic
+ */
+extern DECLSPEC SDL_Haptic *SDLCALL SDL_HapticOpenFromJoystick(SDL_Joystick *
+ joystick);
+
+/**
+ * Close a haptic device previously opened with SDL_HapticOpen().
+ *
+ * \param haptic the SDL_Haptic device to close.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticOpen
+ */
+extern DECLSPEC void SDLCALL SDL_HapticClose(SDL_Haptic * haptic);
+
+/**
+ * Get the number of effects a haptic device can store.
+ *
+ * On some platforms this isn't fully supported, and therefore is an
+ * approximation. Always check to see if your created effect was actually
+ * created and do not rely solely on SDL_HapticNumEffects().
+ *
+ * \param haptic the SDL_Haptic device to query.
+ * \returns the number of effects the haptic device can store or a negative
+ * error code on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticNumEffectsPlaying
+ * \sa SDL_HapticQuery
+ */
+extern DECLSPEC int SDLCALL SDL_HapticNumEffects(SDL_Haptic * haptic);
+
+/**
+ * Get the number of effects a haptic device can play at the same time.
+ *
+ * This is not supported on all platforms, but will always return a value.
+ *
+ * \param haptic the SDL_Haptic device to query maximum playing effects.
+ * \returns the number of effects the haptic device can play at the same time
+ * or a negative error code on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticNumEffects
+ * \sa SDL_HapticQuery
+ */
+extern DECLSPEC int SDLCALL SDL_HapticNumEffectsPlaying(SDL_Haptic * haptic);
+
+/**
+ * Get the haptic device's supported features in bitwise manner.
+ *
+ * \param haptic the SDL_Haptic device to query.
+ * \returns a list of supported haptic features in bitwise manner (OR'd), or 0
+ * on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticEffectSupported
+ * \sa SDL_HapticNumEffects
+ */
+extern DECLSPEC unsigned int SDLCALL SDL_HapticQuery(SDL_Haptic * haptic);
+
+
+/**
+ * Get the number of haptic axes the device has.
+ *
+ * The number of haptic axes might be useful if working with the
+ * SDL_HapticDirection effect.
+ *
+ * \param haptic the SDL_Haptic device to query.
+ * \returns the number of axes on success or a negative error code on failure;
+ * call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC int SDLCALL SDL_HapticNumAxes(SDL_Haptic * haptic);
+
+/**
+ * Check to see if an effect is supported by a haptic device.
+ *
+ * \param haptic the SDL_Haptic device to query.
+ * \param effect the desired effect to query.
+ * \returns SDL_TRUE if effect is supported, SDL_FALSE if it isn't, or a
+ * negative error code on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticNewEffect
+ * \sa SDL_HapticQuery
+ */
+extern DECLSPEC int SDLCALL SDL_HapticEffectSupported(SDL_Haptic * haptic,
+ SDL_HapticEffect *
+ effect);
+
+/**
+ * Create a new haptic effect on a specified device.
+ *
+ * \param haptic an SDL_Haptic device to create the effect on.
+ * \param effect an SDL_HapticEffect structure containing the properties of
+ * the effect to create.
+ * \returns the ID of the effect on success or a negative error code on
+ * failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticDestroyEffect
+ * \sa SDL_HapticRunEffect
+ * \sa SDL_HapticUpdateEffect
+ */
+extern DECLSPEC int SDLCALL SDL_HapticNewEffect(SDL_Haptic * haptic,
+ SDL_HapticEffect * effect);
+
+/**
+ * Update the properties of an effect.
+ *
+ * Can be used dynamically, although behavior when dynamically changing
+ * direction may be strange. Specifically the effect may re-upload itself and
+ * start playing from the start. You also cannot change the type either when
+ * running SDL_HapticUpdateEffect().
+ *
+ * \param haptic the SDL_Haptic device that has the effect.
+ * \param effect the identifier of the effect to update.
+ * \param data an SDL_HapticEffect structure containing the new effect
+ * properties to use.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticDestroyEffect
+ * \sa SDL_HapticNewEffect
+ * \sa SDL_HapticRunEffect
+ */
+extern DECLSPEC int SDLCALL SDL_HapticUpdateEffect(SDL_Haptic * haptic,
+ int effect,
+ SDL_HapticEffect * data);
+
+/**
+ * Run the haptic effect on its associated haptic device.
+ *
+ * To repeat the effect over and over indefinitely, set `iterations` to
+ * `SDL_HAPTIC_INFINITY`. (Repeats the envelope - attack and fade.) To make
+ * one instance of the effect last indefinitely (so the effect does not fade),
+ * set the effect's `length` in its structure/union to `SDL_HAPTIC_INFINITY`
+ * instead.
+ *
+ * \param haptic the SDL_Haptic device to run the effect on.
+ * \param effect the ID of the haptic effect to run.
+ * \param iterations the number of iterations to run the effect; use
+ * `SDL_HAPTIC_INFINITY` to repeat forever.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticDestroyEffect
+ * \sa SDL_HapticGetEffectStatus
+ * \sa SDL_HapticStopEffect
+ */
+extern DECLSPEC int SDLCALL SDL_HapticRunEffect(SDL_Haptic * haptic,
+ int effect,
+ Uint32 iterations);
+
+/**
+ * Stop the haptic effect on its associated haptic device.
+ *
+ * *
+ *
+ * \param haptic the SDL_Haptic device to stop the effect on.
+ * \param effect the ID of the haptic effect to stop.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticDestroyEffect
+ * \sa SDL_HapticRunEffect
+ */
+extern DECLSPEC int SDLCALL SDL_HapticStopEffect(SDL_Haptic * haptic,
+ int effect);
+
+/**
+ * Destroy a haptic effect on the device.
+ *
+ * This will stop the effect if it's running. Effects are automatically
+ * destroyed when the device is closed.
+ *
+ * \param haptic the SDL_Haptic device to destroy the effect on.
+ * \param effect the ID of the haptic effect to destroy.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticNewEffect
+ */
+extern DECLSPEC void SDLCALL SDL_HapticDestroyEffect(SDL_Haptic * haptic,
+ int effect);
+
+/**
+ * Get the status of the current effect on the specified haptic device.
+ *
+ * Device must support the SDL_HAPTIC_STATUS feature.
+ *
+ * \param haptic the SDL_Haptic device to query for the effect status on.
+ * \param effect the ID of the haptic effect to query its status.
+ * \returns 0 if it isn't playing, 1 if it is playing, or a negative error
+ * code on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticRunEffect
+ * \sa SDL_HapticStopEffect
+ */
+extern DECLSPEC int SDLCALL SDL_HapticGetEffectStatus(SDL_Haptic * haptic,
+ int effect);
+
+/**
+ * Set the global gain of the specified haptic device.
+ *
+ * Device must support the SDL_HAPTIC_GAIN feature.
+ *
+ * The user may specify the maximum gain by setting the environment variable
+ * `SDL_HAPTIC_GAIN_MAX` which should be between 0 and 100. All calls to
+ * SDL_HapticSetGain() will scale linearly using `SDL_HAPTIC_GAIN_MAX` as the
+ * maximum.
+ *
+ * \param haptic the SDL_Haptic device to set the gain on.
+ * \param gain value to set the gain to, should be between 0 and 100 (0 -
+ * 100).
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticQuery
+ */
+extern DECLSPEC int SDLCALL SDL_HapticSetGain(SDL_Haptic * haptic, int gain);
+
+/**
+ * Set the global autocenter of the device.
+ *
+ * Autocenter should be between 0 and 100. Setting it to 0 will disable
+ * autocentering.
+ *
+ * Device must support the SDL_HAPTIC_AUTOCENTER feature.
+ *
+ * \param haptic the SDL_Haptic device to set autocentering on.
+ * \param autocenter value to set autocenter to (0-100).
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticQuery
+ */
+extern DECLSPEC int SDLCALL SDL_HapticSetAutocenter(SDL_Haptic * haptic,
+ int autocenter);
+
+/**
+ * Pause a haptic device.
+ *
+ * Device must support the `SDL_HAPTIC_PAUSE` feature. Call
+ * SDL_HapticUnpause() to resume playback.
+ *
+ * Do not modify the effects nor add new ones while the device is paused. That
+ * can cause all sorts of weird errors.
+ *
+ * \param haptic the SDL_Haptic device to pause.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticUnpause
+ */
+extern DECLSPEC int SDLCALL SDL_HapticPause(SDL_Haptic * haptic);
+
+/**
+ * Unpause a haptic device.
+ *
+ * Call to unpause after SDL_HapticPause().
+ *
+ * \param haptic the SDL_Haptic device to unpause.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticPause
+ */
+extern DECLSPEC int SDLCALL SDL_HapticUnpause(SDL_Haptic * haptic);
+
+/**
+ * Stop all the currently playing effects on a haptic device.
+ *
+ * \param haptic the SDL_Haptic device to stop.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC int SDLCALL SDL_HapticStopAll(SDL_Haptic * haptic);
+
+/**
+ * Check whether rumble is supported on a haptic device.
+ *
+ * \param haptic haptic device to check for rumble support.
+ * \returns SDL_TRUE if effect is supported, SDL_FALSE if it isn't, or a
+ * negative error code on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticRumbleInit
+ * \sa SDL_HapticRumblePlay
+ * \sa SDL_HapticRumbleStop
+ */
+extern DECLSPEC int SDLCALL SDL_HapticRumbleSupported(SDL_Haptic * haptic);
+
+/**
+ * Initialize a haptic device for simple rumble playback.
+ *
+ * \param haptic the haptic device to initialize for simple rumble playback.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticOpen
+ * \sa SDL_HapticRumblePlay
+ * \sa SDL_HapticRumbleStop
+ * \sa SDL_HapticRumbleSupported
+ */
+extern DECLSPEC int SDLCALL SDL_HapticRumbleInit(SDL_Haptic * haptic);
+
+/**
+ * Run a simple rumble effect on a haptic device.
+ *
+ * \param haptic the haptic device to play the rumble effect on.
+ * \param strength strength of the rumble to play as a 0-1 float value.
+ * \param length length of the rumble to play in milliseconds.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticRumbleInit
+ * \sa SDL_HapticRumbleStop
+ * \sa SDL_HapticRumbleSupported
+ */
+extern DECLSPEC int SDLCALL SDL_HapticRumblePlay(SDL_Haptic * haptic, float strength, Uint32 length );
+
+/**
+ * Stop the simple rumble on a haptic device.
+ *
+ * \param haptic the haptic device to stop the rumble effect on.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HapticRumbleInit
+ * \sa SDL_HapticRumblePlay
+ * \sa SDL_HapticRumbleSupported
+ */
+extern DECLSPEC int SDLCALL SDL_HapticRumbleStop(SDL_Haptic * haptic);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_haptic_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_hidapi.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_hidapi.h
new file mode 100644
index 00000000..b14442a6
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_hidapi.h
@@ -0,0 +1,443 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* WIKI CATEGORY: HIDAPI */
+
+/**
+ * # CategoryHIDAPI
+ *
+ * Header file for SDL HIDAPI functions.
+ *
+ * This is an adaptation of the original HIDAPI interface by Alan Ott, and
+ * includes source code licensed under the following license:
+ *
+ * ```
+ * HIDAPI - Multi-Platform library for
+ * communication with HID devices.
+ *
+ * Copyright 2009, Alan Ott, Signal 11 Software.
+ * All Rights Reserved.
+ *
+ * This software may be used by anyone for any reason so
+ * long as the copyright notice in the source files
+ * remains intact.
+ * ```
+ *
+ * (Note that this license is the same as item three of SDL's zlib license, so
+ * it adds no new requirements on the user.)
+ *
+ * If you would like a version of SDL without this code, you can build SDL
+ * with SDL_HIDAPI_DISABLED defined to 1. You might want to do this for
+ * example on iOS or tvOS to avoid a dependency on the CoreBluetooth
+ * framework.
+ */
+
+#ifndef SDL_hidapi_h_
+#define SDL_hidapi_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * A handle representing an open HID device
+ */
+struct SDL_hid_device_;
+typedef struct SDL_hid_device_ SDL_hid_device; /**< opaque hidapi structure */
+
+/** hidapi info structure */
+
+/**
+ * Information about a connected HID device
+ */
+typedef struct SDL_hid_device_info
+{
+ /** Platform-specific device path */
+ char *path;
+ /** Device Vendor ID */
+ unsigned short vendor_id;
+ /** Device Product ID */
+ unsigned short product_id;
+ /** Serial Number */
+ wchar_t *serial_number;
+ /** Device Release Number in binary-coded decimal,
+ also known as Device Version Number */
+ unsigned short release_number;
+ /** Manufacturer String */
+ wchar_t *manufacturer_string;
+ /** Product string */
+ wchar_t *product_string;
+ /** Usage Page for this Device/Interface
+ (Windows/Mac only). */
+ unsigned short usage_page;
+ /** Usage for this Device/Interface
+ (Windows/Mac only).*/
+ unsigned short usage;
+ /** The USB interface which this logical device
+ represents.
+
+ * Valid on both Linux implementations in all cases.
+ * Valid on the Windows implementation only if the device
+ contains more than one interface. */
+ int interface_number;
+
+ /** Additional information about the USB interface.
+ Valid on libusb and Android implementations. */
+ int interface_class;
+ int interface_subclass;
+ int interface_protocol;
+
+ /** Pointer to the next device */
+ struct SDL_hid_device_info *next;
+} SDL_hid_device_info;
+
+
+/**
+ * Initialize the HIDAPI library.
+ *
+ * This function initializes the HIDAPI library. Calling it is not strictly
+ * necessary, as it will be called automatically by SDL_hid_enumerate() and
+ * any of the SDL_hid_open_*() functions if it is needed. This function should
+ * be called at the beginning of execution however, if there is a chance of
+ * HIDAPI handles being opened by different threads simultaneously.
+ *
+ * Each call to this function should have a matching call to SDL_hid_exit()
+ *
+ * \returns 0 on success and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_hid_exit
+ */
+extern DECLSPEC int SDLCALL SDL_hid_init(void);
+
+/**
+ * Finalize the HIDAPI library.
+ *
+ * This function frees all of the static data associated with HIDAPI. It
+ * should be called at the end of execution to avoid memory leaks.
+ *
+ * \returns 0 on success and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_hid_init
+ */
+extern DECLSPEC int SDLCALL SDL_hid_exit(void);
+
+/**
+ * Check to see if devices may have been added or removed.
+ *
+ * Enumerating the HID devices is an expensive operation, so you can call this
+ * to see if there have been any system device changes since the last call to
+ * this function. A change in the counter returned doesn't necessarily mean
+ * that anything has changed, but you can call SDL_hid_enumerate() to get an
+ * updated device list.
+ *
+ * Calling this function for the first time may cause a thread or other system
+ * resource to be allocated to track device change notifications.
+ *
+ * \returns a change counter that is incremented with each potential device
+ * change, or 0 if device change detection isn't available.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_hid_enumerate
+ */
+extern DECLSPEC Uint32 SDLCALL SDL_hid_device_change_count(void);
+
+/**
+ * Enumerate the HID Devices.
+ *
+ * This function returns a linked list of all the HID devices attached to the
+ * system which match vendor_id and product_id. If `vendor_id` is set to 0
+ * then any vendor matches. If `product_id` is set to 0 then any product
+ * matches. If `vendor_id` and `product_id` are both set to 0, then all HID
+ * devices will be returned.
+ *
+ * \param vendor_id The Vendor ID (VID) of the types of device to open.
+ * \param product_id The Product ID (PID) of the types of device to open.
+ * \returns a pointer to a linked list of type SDL_hid_device_info, containing
+ * information about the HID devices attached to the system, or NULL
+ * in the case of failure. Free this linked list by calling
+ * SDL_hid_free_enumeration().
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_hid_device_change_count
+ */
+extern DECLSPEC SDL_hid_device_info * SDLCALL SDL_hid_enumerate(unsigned short vendor_id, unsigned short product_id);
+
+/**
+ * Free an enumeration Linked List
+ *
+ * This function frees a linked list created by SDL_hid_enumerate().
+ *
+ * \param devs Pointer to a list of struct_device returned from
+ * SDL_hid_enumerate().
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC void SDLCALL SDL_hid_free_enumeration(SDL_hid_device_info *devs);
+
+/**
+ * Open a HID device using a Vendor ID (VID), Product ID (PID) and optionally
+ * a serial number.
+ *
+ * If `serial_number` is NULL, the first device with the specified VID and PID
+ * is opened.
+ *
+ * \param vendor_id The Vendor ID (VID) of the device to open.
+ * \param product_id The Product ID (PID) of the device to open.
+ * \param serial_number The Serial Number of the device to open (Optionally
+ * NULL).
+ * \returns a pointer to a SDL_hid_device object on success or NULL on
+ * failure.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC SDL_hid_device * SDLCALL SDL_hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number);
+
+/**
+ * Open a HID device by its path name.
+ *
+ * The path name be determined by calling SDL_hid_enumerate(), or a
+ * platform-specific path name can be used (eg: /dev/hidraw0 on Linux).
+ *
+ * \param path The path name of the device to open.
+ * \returns a pointer to a SDL_hid_device object on success or NULL on
+ * failure.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC SDL_hid_device * SDLCALL SDL_hid_open_path(const char *path, int bExclusive);
+
+/**
+ * Write an Output report to a HID device.
+ *
+ * The first byte of `data` must contain the Report ID. For devices which only
+ * support a single report, this must be set to 0x0. The remaining bytes
+ * contain the report data. Since the Report ID is mandatory, calls to
+ * SDL_hid_write() will always contain one more byte than the report contains.
+ * For example, if a hid report is 16 bytes long, 17 bytes must be passed to
+ * SDL_hid_write(), the Report ID (or 0x0, for devices with a single report),
+ * followed by the report data (16 bytes). In this example, the length passed
+ * in would be 17.
+ *
+ * SDL_hid_write() will send the data on the first OUT endpoint, if one
+ * exists. If it does not, it will send the data through the Control Endpoint
+ * (Endpoint 0).
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param data The data to send, including the report number as the first
+ * byte.
+ * \param length The length in bytes of the data to send.
+ * \returns the actual number of bytes written and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_write(SDL_hid_device *dev, const unsigned char *data, size_t length);
+
+/**
+ * Read an Input report from a HID device with timeout.
+ *
+ * Input reports are returned to the host through the INTERRUPT IN endpoint.
+ * The first byte will contain the Report number if the device uses numbered
+ * reports.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param data A buffer to put the read data into.
+ * \param length The number of bytes to read. For devices with multiple
+ * reports, make sure to read an extra byte for the report
+ * number.
+ * \param milliseconds timeout in milliseconds or -1 for blocking wait.
+ * \returns the actual number of bytes read and -1 on error. If no packet was
+ * available to be read within the timeout period, this function
+ * returns 0.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_read_timeout(SDL_hid_device *dev, unsigned char *data, size_t length, int milliseconds);
+
+/**
+ * Read an Input report from a HID device.
+ *
+ * Input reports are returned to the host through the INTERRUPT IN endpoint.
+ * The first byte will contain the Report number if the device uses numbered
+ * reports.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param data A buffer to put the read data into.
+ * \param length The number of bytes to read. For devices with multiple
+ * reports, make sure to read an extra byte for the report
+ * number.
+ * \returns the actual number of bytes read and -1 on error. If no packet was
+ * available to be read and the handle is in non-blocking mode, this
+ * function returns 0.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_read(SDL_hid_device *dev, unsigned char *data, size_t length);
+
+/**
+ * Set the device handle to be non-blocking.
+ *
+ * In non-blocking mode calls to SDL_hid_read() will return immediately with a
+ * value of 0 if there is no data to be read. In blocking mode, SDL_hid_read()
+ * will wait (block) until there is data to read before returning.
+ *
+ * Nonblocking can be turned on and off at any time.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param nonblock enable or not the nonblocking reads - 1 to enable
+ * nonblocking - 0 to disable nonblocking.
+ * \returns 0 on success and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_set_nonblocking(SDL_hid_device *dev, int nonblock);
+
+/**
+ * Send a Feature report to the device.
+ *
+ * Feature reports are sent over the Control endpoint as a Set_Report
+ * transfer. The first byte of `data` must contain the Report ID. For devices
+ * which only support a single report, this must be set to 0x0. The remaining
+ * bytes contain the report data. Since the Report ID is mandatory, calls to
+ * SDL_hid_send_feature_report() will always contain one more byte than the
+ * report contains. For example, if a hid report is 16 bytes long, 17 bytes
+ * must be passed to SDL_hid_send_feature_report(): the Report ID (or 0x0, for
+ * devices which do not use numbered reports), followed by the report data (16
+ * bytes). In this example, the length passed in would be 17.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param data The data to send, including the report number as the first
+ * byte.
+ * \param length The length in bytes of the data to send, including the report
+ * number.
+ * \returns the actual number of bytes written and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_send_feature_report(SDL_hid_device *dev, const unsigned char *data, size_t length);
+
+/**
+ * Get a feature report from a HID device.
+ *
+ * Set the first byte of `data` to the Report ID of the report to be read.
+ * Make sure to allow space for this extra byte in `data`. Upon return, the
+ * first byte will still contain the Report ID, and the report data will start
+ * in data[1].
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param data A buffer to put the read data into, including the Report ID.
+ * Set the first byte of `data` to the Report ID of the report to
+ * be read, or set it to zero if your device does not use numbered
+ * reports.
+ * \param length The number of bytes to read, including an extra byte for the
+ * report ID. The buffer can be longer than the actual report.
+ * \returns the number of bytes read plus one for the report ID (which is
+ * still in the first byte), or -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_get_feature_report(SDL_hid_device *dev, unsigned char *data, size_t length);
+
+/**
+ * Close a HID device.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC void SDLCALL SDL_hid_close(SDL_hid_device *dev);
+
+/**
+ * Get The Manufacturer String from a HID device.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param string A wide string buffer to put the data into.
+ * \param maxlen The length of the buffer in multiples of wchar_t.
+ * \returns 0 on success and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_get_manufacturer_string(SDL_hid_device *dev, wchar_t *string, size_t maxlen);
+
+/**
+ * Get The Product String from a HID device.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param string A wide string buffer to put the data into.
+ * \param maxlen The length of the buffer in multiples of wchar_t.
+ * \returns 0 on success and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_get_product_string(SDL_hid_device *dev, wchar_t *string, size_t maxlen);
+
+/**
+ * Get The Serial Number String from a HID device.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param string A wide string buffer to put the data into.
+ * \param maxlen The length of the buffer in multiples of wchar_t.
+ * \returns 0 on success and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_get_serial_number_string(SDL_hid_device *dev, wchar_t *string, size_t maxlen);
+
+/**
+ * Get a string from a HID device, based on its string index.
+ *
+ * \param dev A device handle returned from SDL_hid_open().
+ * \param string_index The index of the string to get.
+ * \param string A wide string buffer to put the data into.
+ * \param maxlen The length of the buffer in multiples of wchar_t.
+ * \returns 0 on success and -1 on error.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC int SDLCALL SDL_hid_get_indexed_string(SDL_hid_device *dev, int string_index, wchar_t *string, size_t maxlen);
+
+/**
+ * Start or stop a BLE scan on iOS and tvOS to pair Steam Controllers
+ *
+ * \param active SDL_TRUE to start the scan, SDL_FALSE to stop the scan.
+ *
+ * \since This function is available since SDL 2.0.18.
+ */
+extern DECLSPEC void SDLCALL SDL_hid_ble_scan(SDL_bool active);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_hidapi_h_ */
+
+/* vi: set sts=4 ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_hints.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_hints.h
new file mode 100644
index 00000000..6713d01f
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_hints.h
@@ -0,0 +1,3303 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryHints
+ *
+ * Official documentation for SDL configuration variables
+ *
+ * This file contains functions to set and get configuration hints, as well as
+ * listing each of them alphabetically.
+ *
+ * The convention for naming hints is SDL_HINT_X, where "SDL_X" is the
+ * environment variable that can be used to override the default.
+ *
+ * In general these hints are just that - they may or may not be supported or
+ * applicable on any given platform, but they provide a way for an application
+ * or user to give the library a hint as to how they would like the library to
+ * work.
+ */
+
+#ifndef SDL_hints_h_
+#define SDL_hints_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * A variable controlling whether the Android / iOS built-in accelerometer
+ * should be listed as a joystick device.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": The accelerometer is not listed as a joystick
+ * - "1": The accelerometer is available as a 3 axis joystick (the default).
+ */
+#define SDL_HINT_ACCELEROMETER_AS_JOYSTICK "SDL_ACCELEROMETER_AS_JOYSTICK"
+
+/**
+ * Specify the behavior of Alt+Tab while the keyboard is grabbed.
+ *
+ * By default, SDL emulates Alt+Tab functionality while the keyboard is
+ * grabbed and your window is full-screen. This prevents the user from getting
+ * stuck in your application if you've enabled keyboard grab.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": SDL will not handle Alt+Tab. Your application is responsible for
+ * handling Alt+Tab while the keyboard is grabbed.
+ * - "1": SDL will minimize your window when Alt+Tab is pressed (default)
+ */
+#define SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED "SDL_ALLOW_ALT_TAB_WHILE_GRABBED"
+
+/**
+ * If set to "0" then never set the top most bit on a SDL Window, even if the
+ * video mode expects it.
+ *
+ * This is a debugging aid for developers and not expected to be used by end
+ * users. The default is "1"
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": don't allow topmost
+ * - "1": allow topmost
+ */
+#define SDL_HINT_ALLOW_TOPMOST "SDL_ALLOW_TOPMOST"
+
+/**
+ * Android APK expansion main file version.
+ *
+ * Should be a string number like "1", "2" etc.
+ *
+ * Must be set together with
+ * SDL_HINT_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION.
+ *
+ * If both hints were set then SDL_RWFromFile() will look into expansion files
+ * after a given relative path was not found in the internal storage and
+ * assets.
+ *
+ * By default this hint is not set and the APK expansion files are not
+ * searched.
+ */
+#define SDL_HINT_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION "SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION"
+
+/**
+ * Android APK expansion patch file version.
+ *
+ * Should be a string number like "1", "2" etc.
+ *
+ * Must be set together with SDL_HINT_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION.
+ *
+ * If both hints were set then SDL_RWFromFile() will look into expansion files
+ * after a given relative path was not found in the internal storage and
+ * assets.
+ *
+ * By default this hint is not set and the APK expansion files are not
+ * searched.
+ */
+#define SDL_HINT_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION "SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION"
+
+/**
+ * A variable to control whether the event loop will block itself when the app
+ * is paused.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Non blocking.
+ * - "1": Blocking. (default)
+ *
+ * The value should be set before SDL is initialized.
+ */
+#define SDL_HINT_ANDROID_BLOCK_ON_PAUSE "SDL_ANDROID_BLOCK_ON_PAUSE"
+
+/**
+ * A variable to control whether SDL will pause audio in background (Requires
+ * SDL_ANDROID_BLOCK_ON_PAUSE as "Non blocking")
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Non paused.
+ * - "1": Paused. (default)
+ *
+ * The value should be set before SDL is initialized.
+ */
+#define SDL_HINT_ANDROID_BLOCK_ON_PAUSE_PAUSEAUDIO "SDL_ANDROID_BLOCK_ON_PAUSE_PAUSEAUDIO"
+
+/**
+ * A variable to control whether we trap the Android back button to handle it
+ * manually.
+ *
+ * This is necessary for the right mouse button to work on some Android
+ * devices, or to be able to trap the back button for use in your code
+ * reliably. If set to true, the back button will show up as an SDL_KEYDOWN /
+ * SDL_KEYUP pair with a keycode of SDL_SCANCODE_AC_BACK.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Back button will be handled as usual for system. (default)
+ * - "1": Back button will be trapped, allowing you to handle the key press
+ * manually. (This will also let right mouse click work on systems where the
+ * right mouse button functions as back.)
+ *
+ * The value of this hint is used at runtime, so it can be changed at any
+ * time.
+ */
+#define SDL_HINT_ANDROID_TRAP_BACK_BUTTON "SDL_ANDROID_TRAP_BACK_BUTTON"
+
+/**
+ * Specify an application name.
+ *
+ * This hint lets you specify the application name sent to the OS when
+ * required. For example, this will often appear in volume control applets for
+ * audio streams, and in lists of applications which are inhibiting the
+ * screensaver. You should use a string that describes your program ("My Game
+ * 2: The Revenge")
+ *
+ * Setting this to "" or leaving it unset will have SDL use a reasonable
+ * default: probably the application's name or "SDL Application" if SDL
+ * doesn't have any better information.
+ *
+ * Note that, for audio streams, this can be overridden with
+ * SDL_HINT_AUDIO_DEVICE_APP_NAME.
+ *
+ * On targets where this is not supported, this hint does nothing.
+ */
+#define SDL_HINT_APP_NAME "SDL_APP_NAME"
+
+/**
+ * A variable controlling whether controllers used with the Apple TV generate
+ * UI events.
+ *
+ * When UI events are generated by controller input, the app will be
+ * backgrounded when the Apple TV remote's menu button is pressed, and when
+ * the pause or B buttons on gamepads are pressed.
+ *
+ * More information about properly making use of controllers for the Apple TV
+ * can be found here:
+ * https://developer.apple.com/tvos/human-interface-guidelines/remote-and-controllers/
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Controller input does not generate UI events (the default).
+ * - "1": Controller input generates UI events.
+ */
+#define SDL_HINT_APPLE_TV_CONTROLLER_UI_EVENTS "SDL_APPLE_TV_CONTROLLER_UI_EVENTS"
+
+/**
+ * A variable controlling whether the Apple TV remote's joystick axes will
+ * automatically match the rotation of the remote.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Remote orientation does not affect joystick axes (the default).
+ * - "1": Joystick axes are based on the orientation of the remote.
+ */
+#define SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION "SDL_APPLE_TV_REMOTE_ALLOW_ROTATION"
+
+/**
+ * A variable controlling the audio category on iOS and Mac OS X
+ *
+ * This variable can be set to the following values:
+ *
+ * - "ambient": Use the AVAudioSessionCategoryAmbient audio category, will be
+ * muted by the phone mute switch (default)
+ * - "playback": Use the AVAudioSessionCategoryPlayback category
+ *
+ * For more information, see Apple's documentation:
+ * https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html
+ */
+#define SDL_HINT_AUDIO_CATEGORY "SDL_AUDIO_CATEGORY"
+
+/**
+ * Specify an application name for an audio device.
+ *
+ * Some audio backends (such as PulseAudio) allow you to describe your audio
+ * stream. Among other things, this description might show up in a system
+ * control panel that lets the user adjust the volume on specific audio
+ * streams instead of using one giant master volume slider.
+ *
+ * This hints lets you transmit that information to the OS. The contents of
+ * this hint are used while opening an audio device. You should use a string
+ * that describes your program ("My Game 2: The Revenge")
+ *
+ * Setting this to "" or leaving it unset will have SDL use a reasonable
+ * default: this will be the name set with SDL_HINT_APP_NAME, if that hint is
+ * set. Otherwise, it'll probably the application's name or "SDL Application"
+ * if SDL doesn't have any better information.
+ *
+ * On targets where this is not supported, this hint does nothing.
+ */
+#define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME"
+
+/**
+ * Specify an application name for an audio device.
+ *
+ * Some audio backends (such as PulseAudio) allow you to describe your audio
+ * stream. Among other things, this description might show up in a system
+ * control panel that lets the user adjust the volume on specific audio
+ * streams instead of using one giant master volume slider.
+ *
+ * This hints lets you transmit that information to the OS. The contents of
+ * this hint are used while opening an audio device. You should use a string
+ * that describes your what your program is playing ("audio stream" is
+ * probably sufficient in many cases, but this could be useful for something
+ * like "team chat" if you have a headset playing VoIP audio separately).
+ *
+ * Setting this to "" or leaving it unset will have SDL use a reasonable
+ * default: "audio stream" or something similar.
+ *
+ * On targets where this is not supported, this hint does nothing.
+ */
+#define SDL_HINT_AUDIO_DEVICE_STREAM_NAME "SDL_AUDIO_DEVICE_STREAM_NAME"
+
+/**
+ * Specify an application role for an audio device.
+ *
+ * Some audio backends (such as Pipewire) allow you to describe the role of
+ * your audio stream. Among other things, this description might show up in a
+ * system control panel or software for displaying and manipulating media
+ * playback/capture graphs.
+ *
+ * This hints lets you transmit that information to the OS. The contents of
+ * this hint are used while opening an audio device. You should use a string
+ * that describes your what your program is playing (Game, Music, Movie,
+ * etc...).
+ *
+ * Setting this to "" or leaving it unset will have SDL use a reasonable
+ * default: "Game" or something similar.
+ *
+ * On targets where this is not supported, this hint does nothing.
+ */
+#define SDL_HINT_AUDIO_DEVICE_STREAM_ROLE "SDL_AUDIO_DEVICE_STREAM_ROLE"
+
+/**
+ * A variable controlling speed/quality tradeoff of audio resampling.
+ *
+ * If available, SDL can use libsamplerate ( http://www.mega-nerd.com/SRC/ )
+ * to handle audio resampling. There are different resampling modes available
+ * that produce different levels of quality, using more CPU.
+ *
+ * If this hint isn't specified to a valid setting, or libsamplerate isn't
+ * available, SDL will use the default, internal resampling algorithm.
+ *
+ * As of SDL 2.26, SDL_ConvertAudio() respects this hint when libsamplerate is
+ * available.
+ *
+ * This hint is currently only checked at audio subsystem initialization.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0" or "default": Use SDL's internal resampling (Default when not set -
+ * low quality, fast)
+ * - "1" or "fast": Use fast, slightly higher quality resampling, if available
+ * - "2" or "medium": Use medium quality resampling, if available
+ * - "3" or "best": Use high quality resampling, if available
+ */
+#define SDL_HINT_AUDIO_RESAMPLING_MODE "SDL_AUDIO_RESAMPLING_MODE"
+
+/**
+ * A variable controlling whether SDL updates joystick state when getting
+ * input events
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": You'll call SDL_JoystickUpdate() manually
+ * - "1": SDL will automatically call SDL_JoystickUpdate() (default)
+ *
+ * This hint can be toggled on and off at runtime.
+ */
+#define SDL_HINT_AUTO_UPDATE_JOYSTICKS "SDL_AUTO_UPDATE_JOYSTICKS"
+
+/**
+ * A variable controlling whether SDL updates sensor state when getting input
+ * events
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": You'll call SDL_SensorUpdate() manually
+ * - "1": SDL will automatically call SDL_SensorUpdate() (default)
+ *
+ * This hint can be toggled on and off at runtime.
+ */
+#define SDL_HINT_AUTO_UPDATE_SENSORS "SDL_AUTO_UPDATE_SENSORS"
+
+/**
+ * Prevent SDL from using version 4 of the bitmap header when saving BMPs.
+ *
+ * The bitmap header version 4 is required for proper alpha channel support
+ * and SDL will use it when required. Should this not be desired, this hint
+ * can force the use of the 40 byte header version which is supported
+ * everywhere.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Surfaces with a colorkey or an alpha channel are saved to a 32-bit
+ * BMP file with an alpha mask. SDL will use the bitmap header version 4 and
+ * set the alpha mask accordingly.
+ * - "1": Surfaces with a colorkey or an alpha channel are saved to a 32-bit
+ * BMP file without an alpha mask. The alpha channel data will be in the
+ * file, but applications are going to ignore it.
+ *
+ * The default value is "0".
+ */
+#define SDL_HINT_BMP_SAVE_LEGACY_FORMAT "SDL_BMP_SAVE_LEGACY_FORMAT"
+
+/**
+ * Override for SDL_GetDisplayUsableBounds()
+ *
+ * If set, this hint will override the expected results for
+ * SDL_GetDisplayUsableBounds() for display index 0. Generally you don't want
+ * to do this, but this allows an embedded system to request that some of the
+ * screen be reserved for other uses when paired with a well-behaved
+ * application.
+ *
+ * The contents of this hint must be 4 comma-separated integers, the first is
+ * the bounds x, then y, width and height, in that order.
+ */
+#define SDL_HINT_DISPLAY_USABLE_BOUNDS "SDL_DISPLAY_USABLE_BOUNDS"
+
+/**
+ * Disable giving back control to the browser automatically when running with
+ * asyncify
+ *
+ * With -s ASYNCIFY, SDL2 calls emscripten_sleep during operations such as
+ * refreshing the screen or polling events.
+ *
+ * This hint only applies to the emscripten platform
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Disable emscripten_sleep calls (if you give back browser control
+ * manually or use asyncify for other purposes)
+ * - "1": Enable emscripten_sleep calls (the default)
+ */
+#define SDL_HINT_EMSCRIPTEN_ASYNCIFY "SDL_EMSCRIPTEN_ASYNCIFY"
+
+/**
+ * override the binding element for keyboard inputs for Emscripten builds
+ *
+ * This hint only applies to the emscripten platform.
+ *
+ * The variable can be one of:
+ *
+ * - "#window": the javascript window object (this is the default)
+ * - "#document": the javascript document object
+ * - "#screen": the javascript window.screen object
+ * - "#canvas": the WebGL canvas element
+ *
+ * Any other string without a leading # sign applies to the element on the
+ * page with that ID.
+ */
+#define SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT "SDL_EMSCRIPTEN_KEYBOARD_ELEMENT"
+
+/**
+ * A variable that controls whether the on-screen keyboard should be shown
+ * when text input is active
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Do not show the on-screen keyboard
+ * - "1": Show the on-screen keyboard
+ *
+ * The default value is "1". This hint must be set before text input is
+ * activated.
+ */
+#define SDL_HINT_ENABLE_SCREEN_KEYBOARD "SDL_ENABLE_SCREEN_KEYBOARD"
+
+/**
+ * A variable controlling verbosity of the logging of SDL events pushed onto
+ * the internal queue.
+ *
+ * This variable can be set to the following values, from least to most
+ * verbose:
+ *
+ * - "0": Don't log any events (default)
+ * - "1": Log most events (other than the really spammy ones).
+ * - "2": Include mouse and finger motion events.
+ * - "3": Include SDL_SysWMEvent events.
+ *
+ * This is generally meant to be used to debug SDL itself, but can be useful
+ * for application developers that need better visibility into what is going
+ * on in the event queue. Logged events are sent through SDL_Log(), which
+ * means by default they appear on stdout on most platforms or maybe
+ * OutputDebugString() on Windows, and can be funneled by the app with
+ * SDL_LogSetOutputFunction(), etc.
+ *
+ * This hint can be toggled on and off at runtime, if you only need to log
+ * events for a small subset of program execution.
+ */
+#define SDL_HINT_EVENT_LOGGING "SDL_EVENT_LOGGING"
+
+/**
+ * A variable controlling whether raising the window should be done more
+ * forcefully
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": No forcing (the default)
+ * - "1": Extra level of forcing
+ *
+ * At present, this is only an issue under MS Windows, which makes it nearly
+ * impossible to programmatically move a window to the foreground, for
+ * "security" reasons. See http://stackoverflow.com/a/34414846 for a
+ * discussion.
+ */
+#define SDL_HINT_FORCE_RAISEWINDOW "SDL_HINT_FORCE_RAISEWINDOW"
+
+/**
+ * A variable controlling how 3D acceleration is used to accelerate the SDL
+ * screen surface.
+ *
+ * SDL can try to accelerate the SDL screen surface by using streaming
+ * textures with a 3D rendering engine. This variable controls whether and how
+ * this is done.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable 3D acceleration
+ * - "1": Enable 3D acceleration, using the default renderer.
+ * - "X": Enable 3D acceleration, using X where X is one of the valid
+ * rendering drivers. (e.g. "direct3d", "opengl", etc.)
+ *
+ * By default SDL tries to make a best guess for each platform whether to use
+ * acceleration or not.
+ */
+#define SDL_HINT_FRAMEBUFFER_ACCELERATION "SDL_FRAMEBUFFER_ACCELERATION"
+
+/**
+ * A variable that lets you manually hint extra gamecontroller db entries.
+ *
+ * The variable should be newline delimited rows of gamecontroller config
+ * data, see SDL_gamecontroller.h
+ *
+ * This hint must be set before calling SDL_Init(SDL_INIT_GAMECONTROLLER) You
+ * can update mappings after the system is initialized with
+ * SDL_GameControllerMappingForGUID() and SDL_GameControllerAddMapping()
+ */
+#define SDL_HINT_GAMECONTROLLERCONFIG "SDL_GAMECONTROLLERCONFIG"
+
+/**
+ * A variable that lets you provide a file with extra gamecontroller db
+ * entries.
+ *
+ * The file should contain lines of gamecontroller config data, see
+ * SDL_gamecontroller.h
+ *
+ * This hint must be set before calling SDL_Init(SDL_INIT_GAMECONTROLLER) You
+ * can update mappings after the system is initialized with
+ * SDL_GameControllerMappingForGUID() and SDL_GameControllerAddMapping()
+ */
+#define SDL_HINT_GAMECONTROLLERCONFIG_FILE "SDL_GAMECONTROLLERCONFIG_FILE"
+
+/**
+ * A variable that overrides the automatic controller type detection
+ *
+ * The variable should be comma separated entries, in the form: VID/PID=type
+ *
+ * The VID and PID should be hexadecimal with exactly 4 digits, e.g. 0x00fd
+ *
+ * The type should be one of: Xbox360 XboxOne PS3 PS4 PS5 SwitchPro
+ *
+ * This hint affects what driver is used, and must be set before calling
+ * SDL_Init(SDL_INIT_GAMECONTROLLER)
+ */
+#define SDL_HINT_GAMECONTROLLERTYPE "SDL_GAMECONTROLLERTYPE"
+
+/**
+ * A variable containing a list of devices to skip when scanning for game
+ * controllers.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES "SDL_GAMECONTROLLER_IGNORE_DEVICES"
+
+/**
+ * If set, all devices will be skipped when scanning for game controllers
+ * except for the ones listed in this variable.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT "SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT"
+
+/**
+ * If set, game controller face buttons report their values according to their
+ * labels instead of their positional layout.
+ *
+ * For example, on Nintendo Switch controllers, normally you'd get:
+ *
+ * ```
+ * (Y)
+ * (X) (B)
+ * (A)
+ * ```
+ *
+ * but if this hint is set, you'll get:
+ *
+ * ```
+ * (X)
+ * (Y) (A)
+ * (B)
+ * ```
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Report the face buttons by position, as though they were on an Xbox
+ * controller.
+ * - "1": Report the face buttons by label instead of position
+ *
+ * The default value is "1". This hint may be set at any time.
+ */
+#define SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS "SDL_GAMECONTROLLER_USE_BUTTON_LABELS"
+
+/**
+ * A variable controlling whether grabbing input grabs the keyboard
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Grab will affect only the mouse
+ * - "1": Grab will affect mouse and keyboard
+ *
+ * By default SDL will not grab the keyboard so system shortcuts still work.
+ */
+#define SDL_HINT_GRAB_KEYBOARD "SDL_GRAB_KEYBOARD"
+
+/**
+ * A variable containing a list of devices to ignore in SDL_hid_enumerate()
+ *
+ * For example, to ignore the Shanwan DS3 controller and any Valve controller,
+ * you might have the string "0x2563/0x0523,0x28de/0x0000"
+ */
+#define SDL_HINT_HIDAPI_IGNORE_DEVICES "SDL_HIDAPI_IGNORE_DEVICES"
+
+/**
+ * A variable controlling whether the idle timer is disabled on iOS.
+ *
+ * When an iOS app does not receive touches for some time, the screen is
+ * dimmed automatically. For games where the accelerometer is the only input
+ * this is problematic. This functionality can be disabled by setting this
+ * hint.
+ *
+ * As of SDL 2.0.4, SDL_EnableScreenSaver() and SDL_DisableScreenSaver()
+ * accomplish the same thing on iOS. They should be preferred over this hint.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Enable idle timer
+ * - "1": Disable idle timer
+ */
+#define SDL_HINT_IDLE_TIMER_DISABLED "SDL_IOS_IDLE_TIMER_DISABLED"
+
+/**
+ * A variable to control whether certain IMEs should handle text editing
+ * internally instead of sending SDL_TEXTEDITING events.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": SDL_TEXTEDITING events are sent, and it is the application's
+ * responsibility to render the text from these events and differentiate it
+ * somehow from committed text. (default)
+ * - "1": If supported by the IME then SDL_TEXTEDITING events are not sent,
+ * and text that is being composed will be rendered in its own UI.
+ */
+#define SDL_HINT_IME_INTERNAL_EDITING "SDL_IME_INTERNAL_EDITING"
+
+/**
+ * A variable to control whether certain IMEs should show native UI components
+ * (such as the Candidate List) instead of suppressing them.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Native UI components are not display. (default)
+ * - "1": Native UI components are displayed.
+ */
+#define SDL_HINT_IME_SHOW_UI "SDL_IME_SHOW_UI"
+
+/**
+ * A variable to control if extended IME text support is enabled.
+ *
+ * If enabled then SDL_TextEditingExtEvent will be issued if the text would be
+ * truncated otherwise. Additionally SDL_TextInputEvent will be dispatched
+ * multiple times so that it is not truncated.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Legacy behavior. Text can be truncated, no heap allocations.
+ * (default)
+ * - "1": Modern behavior.
+ */
+#define SDL_HINT_IME_SUPPORT_EXTENDED_TEXT "SDL_IME_SUPPORT_EXTENDED_TEXT"
+
+/**
+ * A variable controlling whether the home indicator bar on iPhone X should be
+ * hidden.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": The indicator bar is not hidden (default for windowed applications)
+ * - "1": The indicator bar is hidden and is shown when the screen is touched
+ * (useful for movie playback applications)
+ * - "2": The indicator bar is dim and the first swipe makes it visible and
+ * the second swipe performs the "home" action (default for fullscreen
+ * applications)
+ */
+#define SDL_HINT_IOS_HIDE_HOME_INDICATOR "SDL_IOS_HIDE_HOME_INDICATOR"
+
+/**
+ * A variable that lets you enable joystick (and gamecontroller) events even
+ * when your app is in the background.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Disable joystick & gamecontroller input events when the application
+ * is in the background.
+ * - "1": Enable joystick & gamecontroller input events when the application
+ * is in the background.
+ *
+ * The default value is "0". This hint may be set at any time.
+ */
+#define SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS "SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS"
+
+/**
+ * A variable containing a list of arcade stick style controllers.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_ARCADESTICK_DEVICES "SDL_JOYSTICK_ARCADESTICK_DEVICES"
+
+/**
+ * A variable containing a list of devices that are not arcade stick style
+ * controllers.
+ *
+ * This will override SDL_HINT_JOYSTICK_ARCADESTICK_DEVICES and the built in
+ * device list.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_ARCADESTICK_DEVICES_EXCLUDED "SDL_JOYSTICK_ARCADESTICK_DEVICES_EXCLUDED"
+
+/**
+ * A variable containing a list of devices that should not be considerd
+ * joysticks.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_BLACKLIST_DEVICES "SDL_JOYSTICK_BLACKLIST_DEVICES"
+
+/**
+ * A variable containing a list of devices that should be considered
+ * joysticks.
+ *
+ * This will override SDL_HINT_JOYSTICK_BLACKLIST_DEVICES and the built in
+ * device list.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_BLACKLIST_DEVICES_EXCLUDED "SDL_JOYSTICK_BLACKLIST_DEVICES_EXCLUDED"
+
+/**
+ * A variable containing a list of flightstick style controllers.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_FLIGHTSTICK_DEVICES "SDL_JOYSTICK_FLIGHTSTICK_DEVICES"
+
+/**
+ * A variable containing a list of devices that are not flightstick style
+ * controllers.
+ *
+ * This will override SDL_HINT_JOYSTICK_FLIGHTSTICK_DEVICES and the built in
+ * device list.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_FLIGHTSTICK_DEVICES_EXCLUDED "SDL_JOYSTICK_FLIGHTSTICK_DEVICES_EXCLUDED"
+
+/**
+ * A variable containing a list of devices known to have a GameCube form
+ * factor.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_GAMECUBE_DEVICES "SDL_JOYSTICK_GAMECUBE_DEVICES"
+
+/**
+ * A variable containing a list of devices known not to have a GameCube form
+ * factor.
+ *
+ * This will override SDL_HINT_JOYSTICK_GAMECUBE_DEVICES and the built in
+ * device list.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_GAMECUBE_DEVICES_EXCLUDED "SDL_JOYSTICK_GAMECUBE_DEVICES_EXCLUDED"
+
+/**
+ * A variable controlling whether the HIDAPI joystick drivers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI drivers are not used
+ * - "1": HIDAPI drivers are used (the default)
+ *
+ * This variable is the default for all drivers, but can be overridden by the
+ * hints for specific drivers below.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI "SDL_JOYSTICK_HIDAPI"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Nintendo GameCube
+ * controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_GAMECUBE "SDL_JOYSTICK_HIDAPI_GAMECUBE"
+
+/**
+ * A variable controlling whether "low_frequency_rumble" and
+ * "high_frequency_rumble" is used to implement the GameCube controller's 3
+ * rumble modes, Stop(0), Rumble(1), and StopHard(2) this is useful for
+ * applications that need full compatibility for things like ADSR envelopes.
+ *
+ * Stop is implemented by setting "low_frequency_rumble" to "0" and
+ * "high_frequency_rumble" ">0" Rumble is both at any arbitrary value,
+ * StopHard is implemented by setting both "low_frequency_rumble" and
+ * "high_frequency_rumble" to "0"
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Normal rumble behavior is behavior is used (default)
+ * - "1": Proper GameCube controller rumble behavior is used
+ */
+#define SDL_HINT_JOYSTICK_GAMECUBE_RUMBLE_BRAKE "SDL_JOYSTICK_GAMECUBE_RUMBLE_BRAKE"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Nintendo Switch
+ * Joy-Cons should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS "SDL_JOYSTICK_HIDAPI_JOY_CONS"
+
+/**
+ * A variable controlling whether Nintendo Switch Joy-Con controllers will be
+ * combined into a single Pro-like controller when using the HIDAPI driver
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Left and right Joy-Con controllers will not be combined and each
+ * will be a mini-gamepad
+ * - "1": Left and right Joy-Con controllers will be combined into a single
+ * controller (the default)
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS "SDL_JOYSTICK_HIDAPI_COMBINE_JOY_CONS"
+
+/**
+ * A variable controlling whether Nintendo Switch Joy-Con controllers will be
+ * in vertical mode when using the HIDAPI driver
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Left and right Joy-Con controllers will not be in vertical mode (the
+ * default)
+ * - "1": Left and right Joy-Con controllers will be in vertical mode
+ *
+ * This hint must be set before calling SDL_Init(SDL_INIT_GAMECONTROLLER)
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS "SDL_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Amazon Luna
+ * controllers connected via Bluetooth should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_LUNA "SDL_JOYSTICK_HIDAPI_LUNA"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Nintendo Online
+ * classic controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_NINTENDO_CLASSIC "SDL_JOYSTICK_HIDAPI_NINTENDO_CLASSIC"
+
+/**
+ * A variable controlling whether the HIDAPI driver for NVIDIA SHIELD
+ * controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_SHIELD "SDL_JOYSTICK_HIDAPI_SHIELD"
+
+/**
+ * A variable controlling whether the HIDAPI driver for PS3 controllers should
+ * be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI on macOS, and "0" on
+ * other platforms.
+ *
+ * It is not possible to use this driver on Windows, due to limitations in the
+ * default drivers installed. See https://github.com/ViGEm/DsHidMini for an
+ * alternative driver on Windows.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_PS3 "SDL_JOYSTICK_HIDAPI_PS3"
+
+/**
+ * A variable controlling whether the HIDAPI driver for PS4 controllers should
+ * be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_PS4 "SDL_JOYSTICK_HIDAPI_PS4"
+
+/**
+ * A variable controlling whether extended input reports should be used for
+ * PS4 controllers when using the HIDAPI driver.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": extended reports are not enabled (the default)
+ * - "1": extended reports
+ *
+ * Extended input reports allow rumble on Bluetooth PS4 controllers, but break
+ * DirectInput handling for applications that don't use SDL.
+ *
+ * Once extended reports are enabled, they can not be disabled without power
+ * cycling the controller.
+ *
+ * For compatibility with applications written for versions of SDL prior to
+ * the introduction of PS5 controller support, this value will also control
+ * the state of extended reports on PS5 controllers when the
+ * SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE hint is not explicitly set.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE "SDL_JOYSTICK_HIDAPI_PS4_RUMBLE"
+
+/**
+ * A variable controlling whether the HIDAPI driver for PS5 controllers should
+ * be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_PS5 "SDL_JOYSTICK_HIDAPI_PS5"
+
+/**
+ * A variable controlling whether the player LEDs should be lit to indicate
+ * which player is associated with a PS5 controller.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": player LEDs are not enabled
+ * - "1": player LEDs are enabled (the default)
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED "SDL_JOYSTICK_HIDAPI_PS5_PLAYER_LED"
+
+/**
+ * A variable controlling whether extended input reports should be used for
+ * PS5 controllers when using the HIDAPI driver.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": extended reports are not enabled (the default)
+ * - "1": extended reports
+ *
+ * Extended input reports allow rumble on Bluetooth PS5 controllers, but break
+ * DirectInput handling for applications that don't use SDL.
+ *
+ * Once extended reports are enabled, they can not be disabled without power
+ * cycling the controller.
+ *
+ * For compatibility with applications written for versions of SDL prior to
+ * the introduction of PS5 controller support, this value defaults to the
+ * value of SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE "SDL_JOYSTICK_HIDAPI_PS5_RUMBLE"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Google Stadia
+ * controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_STADIA "SDL_JOYSTICK_HIDAPI_STADIA"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Bluetooth Steam
+ * Controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used for Steam Controllers, which requires
+ * Bluetooth access and may prompt the user for permission on iOS and
+ * Android.
+ *
+ * The default is "0"
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_STEAM "SDL_JOYSTICK_HIDAPI_STEAM"
+
+/**
+ * A variable controlling whether the HIDAPI driver for the Steam Deck builtin
+ * controller should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_STEAMDECK "SDL_JOYSTICK_HIDAPI_STEAMDECK"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Nintendo Switch
+ * controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_SWITCH "SDL_JOYSTICK_HIDAPI_SWITCH"
+
+/**
+ * A variable controlling whether the Home button LED should be turned on when
+ * a Nintendo Switch Pro controller is opened
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": home button LED is turned off
+ * - "1": home button LED is turned on
+ *
+ * By default the Home button LED state is not changed. This hint can also be
+ * set to a floating point value between 0.0 and 1.0 which controls the
+ * brightness of the Home button LED.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED "SDL_JOYSTICK_HIDAPI_SWITCH_HOME_LED"
+
+/**
+ * A variable controlling whether the Home button LED should be turned on when
+ * a Nintendo Switch Joy-Con controller is opened
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": home button LED is turned off
+ * - "1": home button LED is turned on
+ *
+ * By default the Home button LED state is not changed. This hint can also be
+ * set to a floating point value between 0.0 and 1.0 which controls the
+ * brightness of the Home button LED.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_JOYCON_HOME_LED "SDL_JOYSTICK_HIDAPI_JOYCON_HOME_LED"
+
+/**
+ * A variable controlling whether the player LEDs should be lit to indicate
+ * which player is associated with a Nintendo Switch controller.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": player LEDs are not enabled
+ * - "1": player LEDs are enabled (the default)
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED "SDL_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED"
+
+/**
+ * A variable controlling whether the HIDAPI driver for Nintendo Wii and Wii U
+ * controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * This driver doesn't work with the dolphinbar, so the default is SDL_FALSE
+ * for now.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_WII "SDL_JOYSTICK_HIDAPI_WII"
+
+/**
+ * A variable controlling whether the player LEDs should be lit to indicate
+ * which player is associated with a Wii controller.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": player LEDs are not enabled
+ * - "1": player LEDs are enabled (the default)
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED "SDL_JOYSTICK_HIDAPI_WII_PLAYER_LED"
+
+/**
+ * A variable controlling whether the HIDAPI driver for XBox controllers
+ * should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is "0" on Windows, otherwise the value of
+ * SDL_HINT_JOYSTICK_HIDAPI
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_XBOX "SDL_JOYSTICK_HIDAPI_XBOX"
+
+/**
+ * A variable controlling whether the HIDAPI driver for XBox 360 controllers
+ * should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI_XBOX
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_XBOX_360 "SDL_JOYSTICK_HIDAPI_XBOX_360"
+
+/**
+ * A variable controlling whether the player LEDs should be lit to indicate
+ * which player is associated with an Xbox 360 controller.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": player LEDs are not enabled
+ * - "1": player LEDs are enabled (the default)
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED "SDL_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED"
+
+/**
+ * A variable controlling whether the HIDAPI driver for XBox 360 wireless
+ * controllers should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI_XBOX_360
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_WIRELESS "SDL_JOYSTICK_HIDAPI_XBOX_360_WIRELESS"
+
+/**
+ * A variable controlling whether the HIDAPI driver for XBox One controllers
+ * should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": HIDAPI driver is not used
+ * - "1": HIDAPI driver is used
+ *
+ * The default is the value of SDL_HINT_JOYSTICK_HIDAPI_XBOX
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_XBOX_ONE "SDL_JOYSTICK_HIDAPI_XBOX_ONE"
+
+/**
+ * A variable controlling whether the Home button LED should be turned on when
+ * an Xbox One controller is opened
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": home button LED is turned off
+ * - "1": home button LED is turned on
+ *
+ * By default the Home button LED state is not changed. This hint can also be
+ * set to a floating point value between 0.0 and 1.0 which controls the
+ * brightness of the Home button LED. The default brightness is 0.4.
+ */
+#define SDL_HINT_JOYSTICK_HIDAPI_XBOX_ONE_HOME_LED "SDL_JOYSTICK_HIDAPI_XBOX_ONE_HOME_LED"
+
+/**
+ * A variable controlling whether IOKit should be used for controller
+ * handling.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": IOKit is not used
+ * - "1": IOKit is used (the default)
+ */
+#define SDL_HINT_JOYSTICK_IOKIT "SDL_JOYSTICK_IOKIT"
+
+/**
+ * A variable controlling whether GCController should be used for controller
+ * handling.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": GCController is not used
+ * - "1": GCController is used (the default)
+ */
+#define SDL_HINT_JOYSTICK_MFI "SDL_JOYSTICK_MFI"
+
+/**
+ * A variable controlling whether the RAWINPUT joystick drivers should be used
+ * for better handling XInput-capable devices.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": RAWINPUT drivers are not used
+ * - "1": RAWINPUT drivers are used (the default)
+ */
+#define SDL_HINT_JOYSTICK_RAWINPUT "SDL_JOYSTICK_RAWINPUT"
+
+/**
+ * A variable controlling whether the RAWINPUT driver should pull correlated
+ * data from XInput.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": RAWINPUT driver will only use data from raw input APIs
+ * - "1": RAWINPUT driver will also pull data from XInput, providing better
+ * trigger axes, guide button presses, and rumble support for Xbox
+ * controllers
+ *
+ * The default is "1". This hint applies to any joysticks opened after setting
+ * the hint.
+ */
+#define SDL_HINT_JOYSTICK_RAWINPUT_CORRELATE_XINPUT "SDL_JOYSTICK_RAWINPUT_CORRELATE_XINPUT"
+
+/**
+ * A variable controlling whether the ROG Chakram mice should show up as
+ * joysticks
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": ROG Chakram mice do not show up as joysticks (the default)
+ * - "1": ROG Chakram mice show up as joysticks
+ */
+#define SDL_HINT_JOYSTICK_ROG_CHAKRAM "SDL_JOYSTICK_ROG_CHAKRAM"
+
+/**
+ * A variable controlling whether a separate thread should be used for
+ * handling joystick detection and raw input messages on Windows
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": A separate thread is not used (the default)
+ * - "1": A separate thread is used for handling raw input messages
+ */
+#define SDL_HINT_JOYSTICK_THREAD "SDL_JOYSTICK_THREAD"
+
+/**
+ * A variable containing a list of throttle style controllers.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_THROTTLE_DEVICES "SDL_JOYSTICK_THROTTLE_DEVICES"
+
+/**
+ * A variable containing a list of devices that are not throttle style
+ * controllers.
+ *
+ * This will override SDL_HINT_JOYSTICK_THROTTLE_DEVICES and the built in
+ * device list.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_THROTTLE_DEVICES_EXCLUDED "SDL_JOYSTICK_THROTTLE_DEVICES_EXCLUDED"
+
+/**
+ * A variable controlling whether Windows.Gaming.Input should be used for
+ * controller handling.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": WGI is not used
+ * - "1": WGI is used (the default)
+ */
+#define SDL_HINT_JOYSTICK_WGI "SDL_JOYSTICK_WGI"
+
+/**
+ * A variable containing a list of wheel style controllers.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_WHEEL_DEVICES "SDL_JOYSTICK_WHEEL_DEVICES"
+
+/**
+ * A variable containing a list of devices that are not wheel style
+ * controllers.
+ *
+ * This will override SDL_HINT_JOYSTICK_WHEEL_DEVICES and the built in device
+ * list.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_WHEEL_DEVICES_EXCLUDED "SDL_JOYSTICK_WHEEL_DEVICES_EXCLUDED"
+
+/**
+ * A variable containing a list of devices known to have all axes centered at
+ * zero.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_JOYSTICK_ZERO_CENTERED_DEVICES "SDL_JOYSTICK_ZERO_CENTERED_DEVICES"
+
+/**
+ * Determines whether SDL enforces that DRM master is required in order to
+ * initialize the KMSDRM video backend.
+ *
+ * The DRM subsystem has a concept of a "DRM master" which is a DRM client
+ * that has the ability to set planes, set cursor, etc. When SDL is DRM
+ * master, it can draw to the screen using the SDL rendering APIs. Without DRM
+ * master, SDL is still able to process input and query attributes of attached
+ * displays, but it cannot change display state or draw to the screen
+ * directly.
+ *
+ * In some cases, it can be useful to have the KMSDRM backend even if it
+ * cannot be used for rendering. An app may want to use SDL for input
+ * processing while using another rendering API (such as an MMAL overlay on
+ * Raspberry Pi) or using its own code to render to DRM overlays that SDL
+ * doesn't support.
+ *
+ * This hint must be set before initializing the video subsystem.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": SDL will allow usage of the KMSDRM backend without DRM master
+ * - "1": SDL Will require DRM master to use the KMSDRM backend (default)
+ */
+#define SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER "SDL_KMSDRM_REQUIRE_DRM_MASTER"
+
+/**
+ * A comma separated list of devices to open as joysticks
+ *
+ * This variable is currently only used by the Linux joystick driver.
+ */
+#define SDL_HINT_JOYSTICK_DEVICE "SDL_JOYSTICK_DEVICE"
+
+
+/**
+ * A variable containing a list of devices and their desired number of haptic
+ * (force feedback) enabled axis.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form plus the number of desired axes, e.g.
+ *
+ * `0xAAAA/0xBBBB/1,0xCCCC/0xDDDD/3`
+ *
+ * This hint supports a "wildcard" device that will set the number of haptic
+ * axes on all initialized haptic devices which were not defined explicitly in
+ * this hint.
+ *
+ * `0xFFFF/0xFFFF/1`
+ *
+ * This hint should be set before a controller is opened. The number of haptic
+ * axes won't exceed the number of real axes found on the device.
+ */
+#define SDL_HINT_JOYSTICK_HAPTIC_AXES "SDL_JOYSTICK_HAPTIC_AXES"
+
+/**
+ * A variable controlling whether joysticks on Linux will always treat 'hat'
+ * axis inputs (ABS_HAT0X - ABS_HAT3Y) as 8-way digital hats without checking
+ * whether they may be analog.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Only map hat axis inputs to digital hat outputs if the input axes
+ * appear to actually be digital (the default)
+ * - "1": Always handle the input axes numbered ABS_HAT0X to ABS_HAT3Y as
+ * digital hats
+ */
+#define SDL_HINT_LINUX_DIGITAL_HATS "SDL_LINUX_DIGITAL_HATS"
+
+/**
+ * A variable controlling whether digital hats on Linux will apply deadzones
+ * to their underlying input axes or use unfiltered values.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Return digital hat values based on unfiltered input axis values
+ * - "1": Return digital hat values with deadzones on the input axes taken
+ * into account (the default)
+ */
+#define SDL_HINT_LINUX_HAT_DEADZONES "SDL_LINUX_HAT_DEADZONES"
+
+/**
+ * A variable controlling whether to use the classic /dev/input/js* joystick
+ * interface or the newer /dev/input/event* joystick interface on Linux
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use /dev/input/event*
+ * - "1": Use /dev/input/js*
+ *
+ * By default the /dev/input/event* interfaces are used
+ */
+#define SDL_HINT_LINUX_JOYSTICK_CLASSIC "SDL_LINUX_JOYSTICK_CLASSIC"
+
+/**
+ * A variable controlling whether joysticks on Linux adhere to their
+ * HID-defined deadzones or return unfiltered values.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Return unfiltered joystick axis values (the default)
+ * - "1": Return axis values with deadzones taken into account
+ */
+#define SDL_HINT_LINUX_JOYSTICK_DEADZONES "SDL_LINUX_JOYSTICK_DEADZONES"
+
+/**
+ * A variable controlling the default SDL log levels.
+ *
+ * This variable is a comma separated set of category=level tokens that define
+ * the default logging levels for SDL applications.
+ *
+ * The category can be a numeric category, one of "app", "error", "assert",
+ * "system", "audio", "video", "render", "input", "test", or `*` for any
+ * unspecified category.
+ *
+ * The level can be a numeric level, one of "verbose", "debug", "info",
+ * "warn", "error", "critical", or "quiet" to disable that category.
+ *
+ * You can omit the category if you want to set the logging level for all
+ * categories.
+ *
+ * If this hint isn't set, the default log levels are equivalent to:
+ * "app=info,assert=warn,test=verbose,*=error"
+ */
+#define SDL_HINT_LOGGING "SDL_LOGGING"
+
+/**
+ * When set don't force the SDL app to become a foreground process
+ *
+ * This hint only applies to Mac OS X.
+ */
+#define SDL_HINT_MAC_BACKGROUND_APP "SDL_MAC_BACKGROUND_APP"
+
+/**
+ * A variable that determines whether ctrl+click should generate a right-click
+ * event on Mac
+ *
+ * If present, holding ctrl while left clicking will generate a right click
+ * event when on Mac.
+ */
+#define SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK "SDL_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK"
+
+/**
+ * A variable controlling whether dispatching OpenGL context updates should
+ * block the dispatching thread until the main thread finishes processing
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Dispatching OpenGL context updates will block the dispatching thread
+ * until the main thread finishes processing (default).
+ * - "1": Dispatching OpenGL context updates will allow the dispatching thread
+ * to continue execution.
+ *
+ * Generally you want the default, but if you have OpenGL code in a background
+ * thread on a Mac, and the main thread hangs because it's waiting for that
+ * background thread, but that background thread is also hanging because it's
+ * waiting for the main thread to do an update, this might fix your issue.
+ *
+ * This hint only applies to macOS.
+ *
+ * This hint is available since SDL 2.24.0.
+ */
+#define SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH "SDL_MAC_OPENGL_ASYNC_DISPATCH"
+
+/**
+ * A variable setting the double click radius, in pixels.
+ */
+#define SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS "SDL_MOUSE_DOUBLE_CLICK_RADIUS"
+
+/**
+ * A variable setting the double click time, in milliseconds.
+ */
+#define SDL_HINT_MOUSE_DOUBLE_CLICK_TIME "SDL_MOUSE_DOUBLE_CLICK_TIME"
+
+/**
+ * Allow mouse click events when clicking to focus an SDL window
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Ignore mouse clicks that activate a window
+ * - "1": Generate events for mouse clicks that activate a window
+ *
+ * By default SDL will ignore mouse clicks that activate a window
+ */
+#define SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH "SDL_MOUSE_FOCUS_CLICKTHROUGH"
+
+/**
+ * A variable setting the speed scale for mouse motion, in floating point,
+ * when the mouse is not in relative mode
+ */
+#define SDL_HINT_MOUSE_NORMAL_SPEED_SCALE "SDL_MOUSE_NORMAL_SPEED_SCALE"
+
+/**
+ * A variable controlling whether relative mouse mode constrains the mouse to
+ * the center of the window
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Relative mouse mode constrains the mouse to the window
+ * - "1": Relative mouse mode constrains the mouse to the center of the window
+ *
+ * Constraining to the center of the window works better for FPS games and
+ * when the application is running over RDP. Constraining to the whole window
+ * works better for 2D games and increases the chance that the mouse will be
+ * in the correct position when using high DPI mice.
+ *
+ * By default SDL will constrain the mouse to the center of the window
+ */
+#define SDL_HINT_MOUSE_RELATIVE_MODE_CENTER "SDL_MOUSE_RELATIVE_MODE_CENTER"
+
+/**
+ * A variable controlling whether relative mouse mode is implemented using
+ * mouse warping
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Relative mouse mode uses raw input
+ * - "1": Relative mouse mode uses mouse warping
+ *
+ * By default SDL will use raw input for relative mouse mode
+ */
+#define SDL_HINT_MOUSE_RELATIVE_MODE_WARP "SDL_MOUSE_RELATIVE_MODE_WARP"
+
+/**
+ * A variable controlling whether relative mouse motion is affected by
+ * renderer scaling
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Relative motion is unaffected by DPI or renderer's logical size
+ * - "1": Relative motion is scaled according to DPI scaling and logical size
+ *
+ * By default relative mouse deltas are affected by DPI and renderer scaling
+ */
+#define SDL_HINT_MOUSE_RELATIVE_SCALING "SDL_MOUSE_RELATIVE_SCALING"
+
+/**
+ * A variable setting the scale for mouse motion, in floating point, when the
+ * mouse is in relative mode
+ */
+#define SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE "SDL_MOUSE_RELATIVE_SPEED_SCALE"
+
+/**
+ * A variable controlling whether the system mouse acceleration curve is used
+ * for relative mouse motion.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Relative mouse motion will be unscaled (the default)
+ * - "1": Relative mouse motion will be scaled using the system mouse
+ * acceleration curve.
+ *
+ * If SDL_HINT_MOUSE_RELATIVE_SPEED_SCALE is set, that will override the
+ * system speed scale.
+ */
+#define SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE "SDL_MOUSE_RELATIVE_SYSTEM_SCALE"
+
+/**
+ * A variable controlling whether a motion event should be generated for mouse
+ * warping in relative mode.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Warping the mouse will not generate a motion event in relative mode
+ * - "1": Warping the mouse will generate a motion event in relative mode
+ *
+ * By default warping the mouse will not generate motion events in relative
+ * mode. This avoids the application having to filter out large relative
+ * motion due to warping.
+ */
+#define SDL_HINT_MOUSE_RELATIVE_WARP_MOTION "SDL_MOUSE_RELATIVE_WARP_MOTION"
+
+/**
+ * A variable controlling whether the hardware cursor stays visible when
+ * relative mode is active.
+ *
+ * This variable can be set to the following values: "0" - The cursor will be
+ * hidden while relative mode is active (default) "1" - The cursor will remain
+ * visible while relative mode is active
+ *
+ * Note that for systems without raw hardware inputs, relative mode is
+ * implemented using warping, so the hardware cursor will visibly warp between
+ * frames if this is enabled on those systems.
+ */
+#define SDL_HINT_MOUSE_RELATIVE_CURSOR_VISIBLE "SDL_MOUSE_RELATIVE_CURSOR_VISIBLE"
+
+/**
+ * A variable controlling whether mouse events should generate synthetic touch
+ * events
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Mouse events will not generate touch events (default for desktop
+ * platforms)
+ * - "1": Mouse events will generate touch events (default for mobile
+ * platforms, such as Android and iOS)
+ */
+#define SDL_HINT_MOUSE_TOUCH_EVENTS "SDL_MOUSE_TOUCH_EVENTS"
+
+/**
+ * A variable controlling whether the mouse is captured while mouse buttons
+ * are pressed
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": The mouse is not captured while mouse buttons are pressed
+ * - "1": The mouse is captured while mouse buttons are pressed
+ *
+ * By default the mouse is captured while mouse buttons are pressed so if the
+ * mouse is dragged outside the window, the application continues to receive
+ * mouse events until the button is released.
+ */
+#define SDL_HINT_MOUSE_AUTO_CAPTURE "SDL_MOUSE_AUTO_CAPTURE"
+
+/**
+ * Tell SDL not to catch the SIGINT or SIGTERM signals.
+ *
+ * This hint only applies to Unix-like platforms, and should set before any
+ * calls to SDL_Init()
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": SDL will install a SIGINT and SIGTERM handler, and when it catches a
+ * signal, convert it into an SDL_QUIT event.
+ * - "1": SDL will not install a signal handler at all.
+ */
+#define SDL_HINT_NO_SIGNAL_HANDLERS "SDL_NO_SIGNAL_HANDLERS"
+
+/**
+ * A variable controlling what driver to use for OpenGL ES contexts.
+ *
+ * On some platforms, currently Windows and X11, OpenGL drivers may support
+ * creating contexts with an OpenGL ES profile. By default SDL uses these
+ * profiles, when available, otherwise it attempts to load an OpenGL ES
+ * library, e.g. that provided by the ANGLE project. This variable controls
+ * whether SDL follows this default behaviour or will always load an OpenGL ES
+ * library.
+ *
+ * Circumstances where this is useful include - Testing an app with a
+ * particular OpenGL ES implementation, e.g ANGLE, or emulator, e.g. those
+ * from ARM, Imagination or Qualcomm. - Resolving OpenGL ES function addresses
+ * at link time by linking with the OpenGL ES library instead of querying them
+ * at run time with SDL_GL_GetProcAddress().
+ *
+ * Caution: for an application to work with the default behaviour across
+ * different OpenGL drivers it must query the OpenGL ES function addresses at
+ * run time using SDL_GL_GetProcAddress().
+ *
+ * This variable is ignored on most platforms because OpenGL ES is native or
+ * not supported.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use ES profile of OpenGL, if available. (Default when not set.)
+ * - "1": Load OpenGL ES library using the default library names.
+ */
+#define SDL_HINT_OPENGL_ES_DRIVER "SDL_OPENGL_ES_DRIVER"
+
+/**
+ * A variable controlling which orientations are allowed on iOS/Android.
+ *
+ * In some circumstances it is necessary to be able to explicitly control
+ * which UI orientations are allowed.
+ *
+ * This variable is a space delimited list of the following values:
+ *
+ * - "LandscapeLeft"
+ * - "LandscapeRight"
+ * - "Portrait"
+ * - "PortraitUpsideDown"
+ */
+#define SDL_HINT_ORIENTATIONS "SDL_IOS_ORIENTATIONS"
+
+/**
+ * A variable controlling the use of a sentinel event when polling the event
+ * queue
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable poll sentinels
+ * - "1": Enable poll sentinels
+ *
+ * When polling for events, SDL_PumpEvents is used to gather new events from
+ * devices. If a device keeps producing new events between calls to
+ * SDL_PumpEvents, a poll loop will become stuck until the new events stop.
+ * This is most noticeable when moving a high frequency mouse.
+ *
+ * By default, poll sentinels are enabled.
+ */
+#define SDL_HINT_POLL_SENTINEL "SDL_POLL_SENTINEL"
+
+/**
+ * Override for SDL_GetPreferredLocales()
+ *
+ * If set, this will be favored over anything the OS might report for the
+ * user's preferred locales. Changing this hint at runtime will not generate a
+ * SDL_LOCALECHANGED event (but if you can change the hint, you can push your
+ * own event, if you want).
+ *
+ * The format of this hint is a comma-separated list of language and locale,
+ * combined with an underscore, as is a common format: "en_GB". Locale is
+ * optional: "en". So you might have a list like this: "en_GB,jp,es_PT"
+ */
+#define SDL_HINT_PREFERRED_LOCALES "SDL_PREFERRED_LOCALES"
+
+/**
+ * A variable describing the content orientation on QtWayland-based platforms.
+ *
+ * On QtWayland platforms, windows are rotated client-side to allow for custom
+ * transitions. In order to correctly position overlays (e.g. volume bar) and
+ * gestures (e.g. events view, close/minimize gestures), the system needs to
+ * know in which orientation the application is currently drawing its
+ * contents.
+ *
+ * This does not cause the window to be rotated or resized, the application
+ * needs to take care of drawing the content in the right orientation (the
+ * framebuffer is always in portrait mode).
+ *
+ * This variable can be one of the following values:
+ *
+ * - "primary" (default)
+ * - "portrait"
+ * - "landscape"
+ * - "inverted-portrait"
+ * - "inverted-landscape"
+ *
+ * Since SDL 2.0.22 this variable accepts a comma-separated list of values
+ * above.
+ */
+#define SDL_HINT_QTWAYLAND_CONTENT_ORIENTATION "SDL_QTWAYLAND_CONTENT_ORIENTATION"
+
+/**
+ * Flags to set on QtWayland windows to integrate with the native window
+ * manager.
+ *
+ * On QtWayland platforms, this hint controls the flags to set on the windows.
+ * For example, on Sailfish OS "OverridesSystemGestures" disables swipe
+ * gestures.
+ *
+ * This variable is a space-separated list of the following values (empty = no
+ * flags):
+ *
+ * - "OverridesSystemGestures"
+ * - "StaysOnTop"
+ * - "BypassWindowManager"
+ */
+#define SDL_HINT_QTWAYLAND_WINDOW_FLAGS "SDL_QTWAYLAND_WINDOW_FLAGS"
+
+/**
+ * A variable controlling whether the 2D render API is compatible or
+ * efficient.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Don't use batching to make rendering more efficient.
+ * - "1": Use batching, but might cause problems if app makes its own direct
+ * OpenGL calls.
+ *
+ * Up to SDL 2.0.9, the render API would draw immediately when requested. Now
+ * it batches up draw requests and sends them all to the GPU only when forced
+ * to (during SDL_RenderPresent, when changing render targets, by updating a
+ * texture that the batch needs, etc). This is significantly more efficient,
+ * but it can cause problems for apps that expect to render on top of the
+ * render API's output. As such, SDL will disable batching if a specific
+ * render backend is requested (since this might indicate that the app is
+ * planning to use the underlying graphics API directly). This hint can be
+ * used to explicitly request batching in this instance. It is a contract that
+ * you will either never use the underlying graphics API directly, or if you
+ * do, you will call SDL_RenderFlush() before you do so any current batch goes
+ * to the GPU before your work begins. Not following this contract will result
+ * in undefined behavior.
+ */
+#define SDL_HINT_RENDER_BATCHING "SDL_RENDER_BATCHING"
+
+/**
+ * A variable controlling how the 2D render API renders lines
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use the default line drawing method (Bresenham's line algorithm as
+ * of SDL 2.0.20)
+ * - "1": Use the driver point API using Bresenham's line algorithm (correct,
+ * draws many points)
+ * - "2": Use the driver line API (occasionally misses line endpoints based on
+ * hardware driver quirks, was the default before 2.0.20)
+ * - "3": Use the driver geometry API (correct, draws thicker diagonal lines)
+ *
+ * This variable should be set when the renderer is created.
+ */
+#define SDL_HINT_RENDER_LINE_METHOD "SDL_RENDER_LINE_METHOD"
+
+/**
+ * A variable controlling whether to enable Direct3D 11+'s Debug Layer.
+ *
+ * This variable does not have any effect on the Direct3D 9 based renderer.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable Debug Layer use
+ * - "1": Enable Debug Layer use
+ *
+ * By default, SDL does not use Direct3D Debug Layer.
+ */
+#define SDL_HINT_RENDER_DIRECT3D11_DEBUG "SDL_RENDER_DIRECT3D11_DEBUG"
+
+/**
+ * A variable controlling whether the Direct3D device is initialized for
+ * thread-safe operations.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Thread-safety is not enabled (faster)
+ * - "1": Thread-safety is enabled
+ *
+ * By default the Direct3D device is created with thread-safety disabled.
+ */
+#define SDL_HINT_RENDER_DIRECT3D_THREADSAFE "SDL_RENDER_DIRECT3D_THREADSAFE"
+
+/**
+ * A variable specifying which render driver to use.
+ *
+ * If the application doesn't pick a specific renderer to use, this variable
+ * specifies the name of the preferred renderer. If the preferred renderer
+ * can't be initialized, the normal default renderer is used.
+ *
+ * This variable is case insensitive and can be set to the following values:
+ *
+ * - "direct3d"
+ * - "direct3d11"
+ * - "direct3d12"
+ * - "opengl"
+ * - "opengles2"
+ * - "opengles"
+ * - "metal"
+ * - "software"
+ *
+ * The default varies by platform, but it's the first one in the list that is
+ * available on the current platform.
+ */
+#define SDL_HINT_RENDER_DRIVER "SDL_RENDER_DRIVER"
+
+/**
+ * A variable controlling the scaling policy for SDL_RenderSetLogicalSize.
+ *
+ * This variable can be set to the following values:
+ *
+ * "0" or "letterbox" - Uses letterbox/sidebars to fit the entire rendering on
+ * screen "1" or "overscan" - Will zoom the rendering so it fills the entire
+ * screen, allowing edges to be drawn offscreen
+ *
+ * By default letterbox is used
+ */
+#define SDL_HINT_RENDER_LOGICAL_SIZE_MODE "SDL_RENDER_LOGICAL_SIZE_MODE"
+
+/**
+ * A variable controlling whether the OpenGL render driver uses shaders if
+ * they are available.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable shaders
+ * - "1": Enable shaders
+ *
+ * By default shaders are used if OpenGL supports them.
+ */
+#define SDL_HINT_RENDER_OPENGL_SHADERS "SDL_RENDER_OPENGL_SHADERS"
+
+/**
+ * A variable controlling the scaling quality
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0" or "nearest": Nearest pixel sampling
+ * - "1" or "linear": Linear filtering (supported by OpenGL and Direct3D)
+ * - "2" or "best": Currently this is the same as "linear"
+ *
+ * By default nearest pixel sampling is used
+ */
+#define SDL_HINT_RENDER_SCALE_QUALITY "SDL_RENDER_SCALE_QUALITY"
+
+/**
+ * A variable controlling whether updates to the SDL screen surface should be
+ * synchronized with the vertical refresh, to avoid tearing.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable vsync
+ * - "1": Enable vsync
+ *
+ * By default SDL does not sync screen surface updates with vertical refresh.
+ */
+#define SDL_HINT_RENDER_VSYNC "SDL_RENDER_VSYNC"
+
+/**
+ * A variable controlling whether the Metal render driver select low power
+ * device over default one
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use the prefered OS device
+ * - "1": Select a low power one
+ *
+ * By default the prefered OS device is used.
+ */
+#define SDL_HINT_RENDER_METAL_PREFER_LOW_POWER_DEVICE "SDL_RENDER_METAL_PREFER_LOW_POWER_DEVICE"
+
+/**
+ * A variable containing a list of ROG gamepad capable mice.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_ROG_GAMEPAD_MICE "SDL_ROG_GAMEPAD_MICE"
+
+/**
+ * A variable containing a list of devices that are not ROG gamepad capable
+ * mice.
+ *
+ * This will override SDL_HINT_ROG_GAMEPAD_MICE and the built in device list.
+ *
+ * The format of the string is a comma separated list of USB VID/PID pairs in
+ * hexadecimal form, e.g.
+ *
+ * 0xAAAA/0xBBBB,0xCCCC/0xDDDD
+ *
+ * The variable can also take the form of @file, in which case the named file
+ * will be loaded and interpreted as the value of the variable.
+ */
+#define SDL_HINT_ROG_GAMEPAD_MICE_EXCLUDED "SDL_ROG_GAMEPAD_MICE_EXCLUDED"
+
+/**
+ * A variable controlling if VSYNC is automatically disable if doesn't reach
+ * the enough FPS
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": It will be using VSYNC as defined in the main flag. Default
+ * - "1": If VSYNC was previously enabled, then it will disable VSYNC if
+ * doesn't reach enough speed
+ *
+ * By default SDL does not enable the automatic VSYNC
+ */
+#define SDL_HINT_PS2_DYNAMIC_VSYNC "SDL_PS2_DYNAMIC_VSYNC"
+
+/**
+ * A variable to control whether the return key on the soft keyboard should
+ * hide the soft keyboard on Android and iOS.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": The return key will be handled as a key event. This is the behaviour
+ * of SDL <= 2.0.3. (default)
+ * - "1": The return key will hide the keyboard.
+ *
+ * The value of this hint is used at runtime, so it can be changed at any
+ * time.
+ */
+#define SDL_HINT_RETURN_KEY_HIDES_IME "SDL_RETURN_KEY_HIDES_IME"
+
+/**
+ * Tell SDL which Dispmanx layer to use on a Raspberry PI
+ *
+ * Also known as Z-order. The variable can take a negative or positive value.
+ * The default is 10000.
+ */
+#define SDL_HINT_RPI_VIDEO_LAYER "SDL_RPI_VIDEO_LAYER"
+
+/**
+ * Specify an "activity name" for screensaver inhibition.
+ *
+ * Some platforms, notably Linux desktops, list the applications which are
+ * inhibiting the screensaver or other power-saving features.
+ *
+ * This hint lets you specify the "activity name" sent to the OS when
+ * SDL_DisableScreenSaver() is used (or the screensaver is automatically
+ * disabled). The contents of this hint are used when the screensaver is
+ * disabled. You should use a string that describes what your program is doing
+ * (and, therefore, why the screensaver is disabled). For example, "Playing a
+ * game" or "Watching a video".
+ *
+ * Setting this to "" or leaving it unset will have SDL use a reasonable
+ * default: "Playing a game" or something similar.
+ *
+ * On targets where this is not supported, this hint does nothing.
+ */
+#define SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME "SDL_SCREENSAVER_INHIBIT_ACTIVITY_NAME"
+
+/**
+ * Specifies whether SDL_THREAD_PRIORITY_TIME_CRITICAL should be treated as
+ * realtime.
+ *
+ * On some platforms, like Linux, a realtime priority thread may be subject to
+ * restrictions that require special handling by the application. This hint
+ * exists to let SDL know that the app is prepared to handle said
+ * restrictions.
+ *
+ * On Linux, SDL will apply the following configuration to any thread that
+ * becomes realtime:
+ *
+ * - The SCHED_RESET_ON_FORK bit will be set on the scheduling policy.
+ * - An RLIMIT_RTTIME budget will be configured to the rtkit specified limit.
+ * - Exceeding this limit will result in the kernel sending SIGKILL to the
+ * app.
+ *
+ * Refer to the man pages for more information.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": default platform specific behaviour
+ * - "1": Force SDL_THREAD_PRIORITY_TIME_CRITICAL to a realtime scheduling
+ * policy
+ */
+#define SDL_HINT_THREAD_FORCE_REALTIME_TIME_CRITICAL "SDL_THREAD_FORCE_REALTIME_TIME_CRITICAL"
+
+/**
+ * A string specifying additional information to use with
+ * SDL_SetThreadPriority.
+ *
+ * By default SDL_SetThreadPriority will make appropriate system changes in
+ * order to apply a thread priority. For example on systems using pthreads the
+ * scheduler policy is changed automatically to a policy that works well with
+ * a given priority. Code which has specific requirements can override SDL's
+ * default behavior with this hint.
+ *
+ * pthread hint values are "current", "other", "fifo" and "rr". Currently no
+ * other platform hint values are defined but may be in the future.
+ */
+#define SDL_HINT_THREAD_PRIORITY_POLICY "SDL_THREAD_PRIORITY_POLICY"
+
+/**
+ * A string specifying SDL's threads stack size in bytes or "0" for the
+ * backend's default size
+ *
+ * Use this hint in case you need to set SDL's threads stack size to other
+ * than the default. This is specially useful if you build SDL against a non
+ * glibc libc library (such as musl) which provides a relatively small default
+ * thread stack size (a few kilobytes versus the default 8MB glibc uses).
+ * Support for this hint is currently available only in the pthread, Windows,
+ * and PSP backend.
+ *
+ * Instead of this hint, in 2.0.9 and later, you can use
+ * SDL_CreateThreadWithStackSize(). This hint only works with the classic
+ * SDL_CreateThread().
+ */
+#define SDL_HINT_THREAD_STACK_SIZE "SDL_THREAD_STACK_SIZE"
+
+/**
+ * A variable that controls the timer resolution, in milliseconds.
+ *
+ * The higher resolution the timer, the more frequently the CPU services timer
+ * interrupts, and the more precise delays are, but this takes up power and
+ * CPU time. This hint is only used on Windows.
+ *
+ * See this blog post for more information:
+ * http://randomascii.wordpress.com/2013/07/08/windows-timer-resolution-megawatts-wasted/
+ *
+ * If this variable is set to "0", the system timer resolution is not set.
+ *
+ * The default value is "1". This hint may be set at any time.
+ */
+#define SDL_HINT_TIMER_RESOLUTION "SDL_TIMER_RESOLUTION"
+
+/**
+ * A variable controlling whether touch events should generate synthetic mouse
+ * events
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Touch events will not generate mouse events
+ * - "1": Touch events will generate mouse events
+ *
+ * By default SDL will generate mouse events for touch events
+ */
+#define SDL_HINT_TOUCH_MOUSE_EVENTS "SDL_TOUCH_MOUSE_EVENTS"
+
+/**
+ * A variable controlling which touchpad should generate synthetic mouse
+ * events
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Only front touchpad should generate mouse events. Default
+ * - "1": Only back touchpad should generate mouse events.
+ * - "2": Both touchpads should generate mouse events.
+ *
+ * By default SDL will generate mouse events for all touch devices
+ */
+#define SDL_HINT_VITA_TOUCH_MOUSE_DEVICE "SDL_HINT_VITA_TOUCH_MOUSE_DEVICE"
+
+/**
+ * A variable controlling whether the Android / tvOS remotes should be listed
+ * as joystick devices, instead of sending keyboard events.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Remotes send enter/escape/arrow key events
+ * - "1": Remotes are available as 2 axis, 2 button joysticks (the default).
+ */
+#define SDL_HINT_TV_REMOTE_AS_JOYSTICK "SDL_TV_REMOTE_AS_JOYSTICK"
+
+/**
+ * A variable controlling whether the screensaver is enabled.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable screensaver
+ * - "1": Enable screensaver
+ *
+ * By default SDL will disable the screensaver.
+ */
+#define SDL_HINT_VIDEO_ALLOW_SCREENSAVER "SDL_VIDEO_ALLOW_SCREENSAVER"
+
+/**
+ * Tell the video driver that we only want a double buffer.
+ *
+ * By default, most lowlevel 2D APIs will use a triple buffer scheme that
+ * wastes no CPU time on waiting for vsync after issuing a flip, but
+ * introduces a frame of latency. On the other hand, using a double buffer
+ * scheme instead is recommended for cases where low latency is an important
+ * factor because we save a whole frame of latency. We do so by waiting for
+ * vsync immediately after issuing a flip, usually just after eglSwapBuffers
+ * call in the backend's *_SwapWindow function.
+ *
+ * Since it's driver-specific, it's only supported where possible and
+ * implemented. Currently supported the following drivers:
+ *
+ * - Wayland (wayland)
+ * - KMSDRM (kmsdrm)
+ * - Raspberry Pi (raspberrypi)
+ */
+#define SDL_HINT_VIDEO_DOUBLE_BUFFER "SDL_VIDEO_DOUBLE_BUFFER"
+
+/**
+ * A variable controlling whether the EGL window is allowed to be composited
+ * as transparent, rather than opaque.
+ *
+ * Most window systems will always render windows opaque, even if the surface
+ * format has an alpha channel. This is not always true, however, so by
+ * default SDL will try to enforce opaque composition. To override this
+ * behavior, you can set this hint to "1".
+ */
+#define SDL_HINT_VIDEO_EGL_ALLOW_TRANSPARENCY "SDL_VIDEO_EGL_ALLOW_TRANSPARENCY"
+
+/**
+ * A variable controlling whether the graphics context is externally managed.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": SDL will manage graphics contexts that are attached to windows.
+ * - "1": Disable graphics context management on windows.
+ *
+ * By default SDL will manage OpenGL contexts in certain situations. For
+ * example, on Android the context will be automatically saved and restored
+ * when pausing the application. Additionally, some platforms will assume
+ * usage of OpenGL if Vulkan isn't used. Setting this to "1" will prevent this
+ * behavior, which is desireable when the application manages the graphics
+ * context, such as an externally managed OpenGL context or attaching a Vulkan
+ * surface to the window.
+ */
+#define SDL_HINT_VIDEO_EXTERNAL_CONTEXT "SDL_VIDEO_EXTERNAL_CONTEXT"
+
+/**
+ * If set to 1, then do not allow high-DPI windows.
+ *
+ * ("Retina" on Mac and iOS)
+ */
+#define SDL_HINT_VIDEO_HIGHDPI_DISABLED "SDL_VIDEO_HIGHDPI_DISABLED"
+
+/**
+ * A variable that dictates policy for fullscreen Spaces on Mac OS X.
+ *
+ * This hint only applies to Mac OS X.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Disable Spaces support (FULLSCREEN_DESKTOP won't use them and
+ * SDL_WINDOW_RESIZABLE windows won't offer the "fullscreen" button on their
+ * titlebars).
+ * - "1": Enable Spaces support (FULLSCREEN_DESKTOP will use them and
+ * SDL_WINDOW_RESIZABLE windows will offer the "fullscreen" button on their
+ * titlebars).
+ *
+ * The default value is "1". This hint must be set before any windows are
+ * created.
+ */
+#define SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES "SDL_VIDEO_MAC_FULLSCREEN_SPACES"
+
+/**
+ * Minimize your SDL_Window if it loses key focus when in fullscreen mode.
+ *
+ * Defaults to false.
+ */
+#define SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS "SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS"
+
+/**
+ * A variable controlling whether the libdecor Wayland backend is allowed to
+ * be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": libdecor use is disabled.
+ * - "1": libdecor use is enabled (default).
+ *
+ * libdecor is used over xdg-shell when xdg-decoration protocol is
+ * unavailable.
+ */
+#define SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR "SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR"
+
+/**
+ * A variable controlling whether the libdecor Wayland backend is preferred
+ * over native decorations.
+ *
+ * When this hint is set, libdecor will be used to provide window decorations,
+ * even if xdg-decoration is available. (Note that, by default, libdecor will
+ * use xdg-decoration itself if available).
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": libdecor is enabled only if server-side decorations are unavailable.
+ * - "1": libdecor is always enabled if available.
+ *
+ * libdecor is used over xdg-shell when xdg-decoration protocol is
+ * unavailable.
+ */
+#define SDL_HINT_VIDEO_WAYLAND_PREFER_LIBDECOR "SDL_VIDEO_WAYLAND_PREFER_LIBDECOR"
+
+/**
+ * A variable controlling whether video mode emulation is enabled under
+ * Wayland.
+ *
+ * When this hint is set, a standard set of emulated CVT video modes will be
+ * exposed for use by the application. If it is disabled, the only modes
+ * exposed will be the logical desktop size and, in the case of a scaled
+ * desktop, the native display resolution.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Video mode emulation is disabled.
+ * - "1": Video mode emulation is enabled.
+ *
+ * By default video mode emulation is enabled.
+ */
+#define SDL_HINT_VIDEO_WAYLAND_MODE_EMULATION "SDL_VIDEO_WAYLAND_MODE_EMULATION"
+
+/**
+ * Enable or disable mouse pointer warp emulation, needed by some older games.
+ *
+ * When this hint is set, any SDL will emulate mouse warps using relative
+ * mouse mode. This is required for some older games (such as Source engine
+ * games), which warp the mouse to the centre of the screen rather than using
+ * relative mouse motion. Note that relative mouse mode may have different
+ * mouse acceleration behaviour than pointer warps.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": All mouse warps fail, as mouse warping is not available under
+ * wayland.
+ * - "1": Some mouse warps will be emulated by forcing relative mouse mode.
+ *
+ * If not set, this is automatically enabled unless an application uses
+ * relative mouse mode directly.
+ */
+#define SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP "SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP"
+
+/**
+ * A variable that is the address of another SDL_Window* (as a hex string
+ * formatted with "%p").
+ *
+ * If this hint is set before SDL_CreateWindowFrom() and the SDL_Window* it is
+ * set to has SDL_WINDOW_OPENGL set (and running on WGL only, currently), then
+ * two things will occur on the newly created SDL_Window:
+ *
+ * 1. Its pixel format will be set to the same pixel format as this
+ * SDL_Window. This is needed for example when sharing an OpenGL context
+ * across multiple windows.
+ *
+ * 2. The flag SDL_WINDOW_OPENGL will be set on the new window so it can be
+ * used for OpenGL rendering.
+ *
+ * This variable can be set to the following values: The address (as a string
+ * "%p") of the SDL_Window* that new windows created with
+ * SDL_CreateWindowFrom() should share a pixel format with.
+ */
+#define SDL_HINT_VIDEO_WINDOW_SHARE_PIXEL_FORMAT "SDL_VIDEO_WINDOW_SHARE_PIXEL_FORMAT"
+
+/**
+ * When calling SDL_CreateWindowFrom(), make the window compatible with
+ * OpenGL.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Don't add any graphics flags to the SDL_WindowFlags
+ * - "1": Add SDL_WINDOW_OPENGL to the SDL_WindowFlags
+ *
+ * By default SDL will not make the foreign window compatible with OpenGL.
+ */
+#define SDL_HINT_VIDEO_FOREIGN_WINDOW_OPENGL "SDL_VIDEO_FOREIGN_WINDOW_OPENGL"
+
+/**
+ * When calling SDL_CreateWindowFrom(), make the window compatible with
+ * Vulkan.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Don't add any graphics flags to the SDL_WindowFlags
+ * - "1": Add SDL_WINDOW_VULKAN to the SDL_WindowFlags
+ *
+ * By default SDL will not make the foreign window compatible with Vulkan.
+ */
+#define SDL_HINT_VIDEO_FOREIGN_WINDOW_VULKAN "SDL_VIDEO_FOREIGN_WINDOW_VULKAN"
+
+/**
+ * A variable specifying which shader compiler to preload when using the
+ * Chrome ANGLE binaries
+ *
+ * SDL has EGL and OpenGL ES2 support on Windows via the ANGLE project. It can
+ * use two different sets of binaries, those compiled by the user from source
+ * or those provided by the Chrome browser. In the later case, these binaries
+ * require that SDL loads a DLL providing the shader compiler.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "d3dcompiler_46.dll: default, best for Vista or later.
+ * - "d3dcompiler_43.dll: for XP support.
+ * - "none": do not load any library, useful if you compiled ANGLE from source
+ * and included the compiler in your binaries.
+ */
+#define SDL_HINT_VIDEO_WIN_D3DCOMPILER "SDL_VIDEO_WIN_D3DCOMPILER"
+
+/**
+ * A variable controlling whether X11 should use GLX or EGL by default
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use GLX
+ * - "1": Use EGL
+ *
+ * By default SDL will use GLX when both are present.
+ */
+#define SDL_HINT_VIDEO_X11_FORCE_EGL "SDL_VIDEO_X11_FORCE_EGL"
+
+/**
+ * A variable controlling whether the X11 _NET_WM_BYPASS_COMPOSITOR hint
+ * should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable _NET_WM_BYPASS_COMPOSITOR
+ * - "1": Enable _NET_WM_BYPASS_COMPOSITOR
+ *
+ * By default SDL will use _NET_WM_BYPASS_COMPOSITOR
+ */
+#define SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR "SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR"
+
+/**
+ * A variable controlling whether the X11 _NET_WM_PING protocol should be
+ * supported.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable _NET_WM_PING
+ * - "1": Enable _NET_WM_PING
+ *
+ * By default SDL will use _NET_WM_PING, but for applications that know they
+ * will not always be able to respond to ping requests in a timely manner they
+ * can turn it off to avoid the window manager thinking the app is hung. The
+ * hint is checked in CreateWindow.
+ */
+#define SDL_HINT_VIDEO_X11_NET_WM_PING "SDL_VIDEO_X11_NET_WM_PING"
+
+/**
+ * A variable forcing the visual ID chosen for new X11 windows
+ */
+#define SDL_HINT_VIDEO_X11_WINDOW_VISUALID "SDL_VIDEO_X11_WINDOW_VISUALID"
+
+/**
+ * A no-longer-used variable controlling whether the X11 Xinerama extension
+ * should be used.
+ *
+ * Before SDL 2.0.24, this would let apps and users disable Xinerama support
+ * on X11. Now SDL never uses Xinerama, and does not check for this hint at
+ * all. The preprocessor define is left here for source compatibility.
+ */
+#define SDL_HINT_VIDEO_X11_XINERAMA "SDL_VIDEO_X11_XINERAMA"
+
+/**
+ * A variable controlling whether the X11 XRandR extension should be used.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Disable XRandR
+ * - "1": Enable XRandR
+ *
+ * By default SDL will use XRandR.
+ */
+#define SDL_HINT_VIDEO_X11_XRANDR "SDL_VIDEO_X11_XRANDR"
+
+/**
+ * A no-longer-used variable controlling whether the X11 VidMode extension
+ * should be used.
+ *
+ * Before SDL 2.0.24, this would let apps and users disable XVidMode support
+ * on X11. Now SDL never uses XVidMode, and does not check for this hint at
+ * all. The preprocessor define is left here for source compatibility.
+ */
+#define SDL_HINT_VIDEO_X11_XVIDMODE "SDL_VIDEO_X11_XVIDMODE"
+
+/**
+ * Controls how the fact chunk affects the loading of a WAVE file.
+ *
+ * The fact chunk stores information about the number of samples of a WAVE
+ * file. The Standards Update from Microsoft notes that this value can be used
+ * to 'determine the length of the data in seconds'. This is especially useful
+ * for compressed formats (for which this is a mandatory chunk) if they
+ * produce multiple sample frames per block and truncating the block is not
+ * allowed. The fact chunk can exactly specify how many sample frames there
+ * should be in this case.
+ *
+ * Unfortunately, most application seem to ignore the fact chunk and so SDL
+ * ignores it by default as well.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "truncate": Use the number of samples to truncate the wave data if the
+ * fact chunk is present and valid
+ * - "strict": Like "truncate", but raise an error if the fact chunk is
+ * invalid, not present for non-PCM formats, or if the data chunk doesn't
+ * have that many samples
+ * - "ignorezero": Like "truncate", but ignore fact chunk if the number of
+ * samples is zero
+ * - "ignore": Ignore fact chunk entirely (default)
+ */
+#define SDL_HINT_WAVE_FACT_CHUNK "SDL_WAVE_FACT_CHUNK"
+
+/**
+ * Controls how the size of the RIFF chunk affects the loading of a WAVE file.
+ *
+ * The size of the RIFF chunk (which includes all the sub-chunks of the WAVE
+ * file) is not always reliable. In case the size is wrong, it's possible to
+ * just ignore it and step through the chunks until a fixed limit is reached.
+ *
+ * Note that files that have trailing data unrelated to the WAVE file or
+ * corrupt files may slow down the loading process without a reliable
+ * boundary. By default, SDL stops after 10000 chunks to prevent wasting time.
+ * Use the environment variable SDL_WAVE_CHUNK_LIMIT to adjust this value.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "force": Always use the RIFF chunk size as a boundary for the chunk
+ * search
+ * - "ignorezero": Like "force", but a zero size searches up to 4 GiB
+ * (default)
+ * - "ignore": Ignore the RIFF chunk size and always search up to 4 GiB
+ * - "maximum": Search for chunks until the end of file (not recommended)
+ */
+#define SDL_HINT_WAVE_RIFF_CHUNK_SIZE "SDL_WAVE_RIFF_CHUNK_SIZE"
+
+/**
+ * Controls how a truncated WAVE file is handled.
+ *
+ * A WAVE file is considered truncated if any of the chunks are incomplete or
+ * the data chunk size is not a multiple of the block size. By default, SDL
+ * decodes until the first incomplete block, as most applications seem to do.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "verystrict": Raise an error if the file is truncated
+ * - "strict": Like "verystrict", but the size of the RIFF chunk is ignored
+ * - "dropframe": Decode until the first incomplete sample frame
+ * - "dropblock": Decode until the first incomplete block (default)
+ */
+#define SDL_HINT_WAVE_TRUNCATION "SDL_WAVE_TRUNCATION"
+
+/**
+ * Tell SDL not to name threads on Windows with the 0x406D1388 Exception.
+ *
+ * The 0x406D1388 Exception is a trick used to inform Visual Studio of a
+ * thread's name, but it tends to cause problems with other debuggers, and the
+ * .NET runtime. Note that SDL 2.0.6 and later will still use the (safer)
+ * SetThreadDescription API, introduced in the Windows 10 Creators Update, if
+ * available.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": SDL will raise the 0x406D1388 Exception to name threads. This is the
+ * default behavior of SDL <= 2.0.4.
+ * - "1": SDL will not raise this exception, and threads will be unnamed.
+ * (default) This is necessary with .NET languages or debuggers that aren't
+ * Visual Studio.
+ */
+#define SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING "SDL_WINDOWS_DISABLE_THREAD_NAMING"
+
+/**
+ * Controls whether menus can be opened with their keyboard shortcut
+ * (Alt+mnemonic).
+ *
+ * If the mnemonics are enabled, then menus can be opened by pressing the Alt
+ * key and the corresponding mnemonic (for example, Alt+F opens the File
+ * menu). However, in case an invalid mnemonic is pressed, Windows makes an
+ * audible beep to convey that nothing happened. This is true even if the
+ * window has no menu at all!
+ *
+ * Because most SDL applications don't have menus, and some want to use the
+ * Alt key for other purposes, SDL disables mnemonics (and the beeping) by
+ * default.
+ *
+ * Note: This also affects keyboard events: with mnemonics enabled, when a
+ * menu is opened from the keyboard, you will not receive a KEYUP event for
+ * the mnemonic key, and *might* not receive one for Alt.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Alt+mnemonic does nothing, no beeping. (default)
+ * - "1": Alt+mnemonic opens menus, invalid mnemonics produce a beep.
+ */
+#define SDL_HINT_WINDOWS_ENABLE_MENU_MNEMONICS "SDL_WINDOWS_ENABLE_MENU_MNEMONICS"
+
+/**
+ * A variable controlling whether the windows message loop is processed by SDL
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": The window message loop is not run
+ * - "1": The window message loop is processed in SDL_PumpEvents()
+ *
+ * By default SDL will process the windows message loop
+ */
+#define SDL_HINT_WINDOWS_ENABLE_MESSAGELOOP "SDL_WINDOWS_ENABLE_MESSAGELOOP"
+
+/**
+ * Force SDL to use Critical Sections for mutexes on Windows.
+ *
+ * On Windows 7 and newer, Slim Reader/Writer Locks are available. They offer
+ * better performance, allocate no kernel resources and use less memory. SDL
+ * will fall back to Critical Sections on older OS versions or if forced to by
+ * this hint.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use SRW Locks when available. If not, fall back to Critical
+ * Sections. (default)
+ * - "1": Force the use of Critical Sections in all cases.
+ */
+#define SDL_HINT_WINDOWS_FORCE_MUTEX_CRITICAL_SECTIONS "SDL_WINDOWS_FORCE_MUTEX_CRITICAL_SECTIONS"
+
+/**
+ * Force SDL to use Kernel Semaphores on Windows.
+ *
+ * Kernel Semaphores are inter-process and require a context switch on every
+ * interaction. On Windows 8 and newer, the WaitOnAddress API is available.
+ * Using that and atomics to implement semaphores increases performance. SDL
+ * will fall back to Kernel Objects on older OS versions or if forced to by
+ * this hint.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use Atomics and WaitOnAddress API when available. If not, fall back
+ * to Kernel Objects. (default)
+ * - "1": Force the use of Kernel Objects in all cases.
+ */
+#define SDL_HINT_WINDOWS_FORCE_SEMAPHORE_KERNEL "SDL_WINDOWS_FORCE_SEMAPHORE_KERNEL"
+
+/**
+ * A variable to specify custom icon resource id from RC file on Windows
+ * platform
+ */
+#define SDL_HINT_WINDOWS_INTRESOURCE_ICON "SDL_WINDOWS_INTRESOURCE_ICON"
+
+/**
+ * A variable to specify custom icon resource id from RC file on Windows
+ * platform
+ */
+#define SDL_HINT_WINDOWS_INTRESOURCE_ICON_SMALL "SDL_WINDOWS_INTRESOURCE_ICON_SMALL"
+
+/**
+ * Tell SDL not to generate window-close events for Alt+F4 on Windows.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": SDL will generate a window-close event when it sees Alt+F4.
+ * - "1": SDL will only do normal key handling for Alt+F4.
+ */
+#define SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4 "SDL_WINDOWS_NO_CLOSE_ON_ALT_F4"
+
+/**
+ * Use the D3D9Ex API introduced in Windows Vista, instead of normal D3D9.
+ *
+ * Direct3D 9Ex contains changes to state management that can eliminate device
+ * loss errors during scenarios like Alt+Tab or UAC prompts. D3D9Ex may
+ * require some changes to your application to cope with the new behavior, so
+ * this is disabled by default.
+ *
+ * This hint must be set before initializing the video subsystem.
+ *
+ * For more information on Direct3D 9Ex, see: -
+ * https://docs.microsoft.com/en-us/windows/win32/direct3darticles/graphics-apis-in-windows-vista#direct3d-9ex
+ * -
+ * https://docs.microsoft.com/en-us/windows/win32/direct3darticles/direct3d-9ex-improvements
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": Use the original Direct3D 9 API (default)
+ * - "1": Use the Direct3D 9Ex API on Vista and later (and fall back if D3D9Ex
+ * is unavailable)
+ */
+#define SDL_HINT_WINDOWS_USE_D3D9EX "SDL_WINDOWS_USE_D3D9EX"
+
+/**
+ * Controls whether SDL will declare the process to be DPI aware.
+ *
+ * This hint must be set before initializing the video subsystem.
+ *
+ * The main purpose of declaring DPI awareness is to disable OS bitmap scaling
+ * of SDL windows on monitors with a DPI scale factor.
+ *
+ * This hint is equivalent to requesting DPI awareness via external means
+ * (e.g. calling SetProcessDpiAwarenessContext) and does not cause SDL to use
+ * a virtualized coordinate system, so it will generally give you 1 SDL
+ * coordinate = 1 pixel even on high-DPI displays.
+ *
+ * For more information, see:
+ * https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
+ *
+ * This variable can be set to the following values:
+ *
+ * - "": Do not change the DPI awareness (default).
+ * - "unaware": Declare the process as DPI unaware. (Windows 8.1 and later).
+ * - "system": Request system DPI awareness. (Vista and later).
+ * - "permonitor": Request per-monitor DPI awareness. (Windows 8.1 and later).
+ * - "permonitorv2": Request per-monitor V2 DPI awareness. (Windows 10,
+ * version 1607 and later). The most visible difference from "permonitor" is
+ * that window title bar will be scaled to the visually correct size when
+ * dragging between monitors with different scale factors. This is the
+ * preferred DPI awareness level.
+ *
+ * If the requested DPI awareness is not available on the currently running
+ * OS, SDL will try to request the best available match.
+ */
+#define SDL_HINT_WINDOWS_DPI_AWARENESS "SDL_WINDOWS_DPI_AWARENESS"
+
+/**
+ * Uses DPI-scaled points as the SDL coordinate system on Windows.
+ *
+ * This changes the SDL coordinate system units to be DPI-scaled points,
+ * rather than pixels everywhere. This means windows will be appropriately
+ * sized, even when created on high-DPI displays with scaling.
+ *
+ * e.g. requesting a 640x480 window from SDL, on a display with 125% scaling
+ * in Windows display settings, will create a window with an 800x600 client
+ * area (in pixels).
+ *
+ * Setting this to "1" implicitly requests process DPI awareness (setting
+ * SDL_WINDOWS_DPI_AWARENESS is unnecessary), and forces
+ * SDL_WINDOW_ALLOW_HIGHDPI on all windows.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": SDL coordinates equal Windows coordinates. No automatic window
+ * resizing when dragging between monitors with different scale factors
+ * (unless this is performed by Windows itself, which is the case when the
+ * process is DPI unaware).
+ * - "1": SDL coordinates are in DPI-scaled points. Automatically resize
+ * windows as needed on displays with non-100% scale factors.
+ */
+#define SDL_HINT_WINDOWS_DPI_SCALING "SDL_WINDOWS_DPI_SCALING"
+
+/**
+ * A variable controlling whether the window frame and title bar are
+ * interactive when the cursor is hidden
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": The window frame is not interactive when the cursor is hidden (no
+ * move, resize, etc)
+ * - "1": The window frame is interactive when the cursor is hidden
+ *
+ * By default SDL will allow interaction with the window frame when the cursor
+ * is hidden
+ */
+#define SDL_HINT_WINDOW_FRAME_USABLE_WHILE_CURSOR_HIDDEN "SDL_WINDOW_FRAME_USABLE_WHILE_CURSOR_HIDDEN"
+
+/**
+ * A variable controlling whether the window is activated when the
+ * SDL_ShowWindow function is called
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": The window is activated when the SDL_ShowWindow function is called
+ * - "1": The window is not activated when the SDL_ShowWindow function is
+ * called
+ *
+ * By default SDL will activate the window when the SDL_ShowWindow function is
+ * called
+ */
+#define SDL_HINT_WINDOW_NO_ACTIVATION_WHEN_SHOWN "SDL_WINDOW_NO_ACTIVATION_WHEN_SHOWN"
+
+/** Allows back-button-press events on Windows Phone to be marked as handled
+ *
+ * Windows Phone devices typically feature a Back button. When pressed,
+ * the OS will emit back-button-press events, which apps are expected to
+ * handle in an appropriate manner. If apps do not explicitly mark these
+ * events as 'Handled', then the OS will invoke its default behavior for
+ * unhandled back-button-press events, which on Windows Phone 8 and 8.1 is to
+ * terminate the app (and attempt to switch to the previous app, or to the
+ * device's home screen).
+ *
+ * Setting the SDL_HINT_WINRT_HANDLE_BACK_BUTTON hint to "1" will cause SDL
+ * to mark back-button-press events as Handled, if and when one is sent to
+ * the app.
+ *
+ * Internally, Windows Phone sends back button events as parameters to
+ * special back-button-press callback functions. Apps that need to respond
+ * to back-button-press events are expected to register one or more
+ * callback functions for such, shortly after being launched (during the
+ * app's initialization phase). After the back button is pressed, the OS
+ * will invoke these callbacks. If the app's callback(s) do not explicitly
+ * mark the event as handled by the time they return, or if the app never
+ * registers one of these callback, the OS will consider the event
+ * un-handled, and it will apply its default back button behavior (terminate
+ * the app).
+ *
+ * SDL registers its own back-button-press callback with the Windows Phone
+ * OS. This callback will emit a pair of SDL key-press events (SDL_KEYDOWN
+ * and SDL_KEYUP), each with a scancode of SDL_SCANCODE_AC_BACK, after which
+ * it will check the contents of the hint, SDL_HINT_WINRT_HANDLE_BACK_BUTTON.
+ * If the hint's value is set to "1", the back button event's Handled
+ * property will get set to 'true'. If the hint's value is set to something
+ * else, or if it is unset, SDL will leave the event's Handled property
+ * alone. (By default, the OS sets this property to 'false', to note.)
+ *
+ * SDL apps can either set SDL_HINT_WINRT_HANDLE_BACK_BUTTON well before a
+ * back button is pressed, or can set it in direct-response to a back button
+ * being pressed.
+ *
+ * In order to get notified when a back button is pressed, SDL apps should
+ * register a callback function with SDL_AddEventWatch(), and have it listen
+ * for SDL_KEYDOWN events that have a scancode of SDL_SCANCODE_AC_BACK.
+ * (Alternatively, SDL_KEYUP events can be listened-for. Listening for
+ * either event type is suitable.) Any value of SDL_HINT_WINRT_HANDLE_BACK_BUTTON
+ * set by such a callback, will be applied to the OS' current
+ * back-button-press event.
+ *
+ * More details on back button behavior in Windows Phone apps can be found
+ * at the following page, on Microsoft's developer site:
+ *
+ * http://msdn.microsoft.com/en-us/library/windowsphone/develop/jj247550(v=vs.105).aspx
+ */
+#define SDL_HINT_WINRT_HANDLE_BACK_BUTTON "SDL_WINRT_HANDLE_BACK_BUTTON"
+
+/** Label text for a WinRT app's privacy policy link
+ *
+ * Network-enabled WinRT apps must include a privacy policy. On Windows 8, 8.1, and RT,
+ * Microsoft mandates that this policy be available via the Windows Settings charm.
+ * SDL provides code to add a link there, with its label text being set via the
+ * optional hint, SDL_HINT_WINRT_PRIVACY_POLICY_LABEL.
+ *
+ * Please note that a privacy policy's contents are not set via this hint. A separate
+ * hint, SDL_HINT_WINRT_PRIVACY_POLICY_URL, is used to link to the actual text of the
+ * policy.
+ *
+ * The contents of this hint should be encoded as a UTF8 string.
+ *
+ * The default value is "Privacy Policy". This hint should only be set during app
+ * initialization, preferably before any calls to SDL_Init().
+ *
+ * For additional information on linking to a privacy policy, see the documentation for
+ * SDL_HINT_WINRT_PRIVACY_POLICY_URL.
+ */
+#define SDL_HINT_WINRT_PRIVACY_POLICY_LABEL "SDL_WINRT_PRIVACY_POLICY_LABEL"
+
+/**
+ * A URL to a WinRT app's privacy policy
+ *
+ * All network-enabled WinRT apps must make a privacy policy available to its
+ * users. On Windows 8, 8.1, and RT, Microsoft mandates that this policy be be
+ * available in the Windows Settings charm, as accessed from within the app.
+ * SDL provides code to add a URL-based link there, which can point to the
+ * app's privacy policy.
+ *
+ * To setup a URL to an app's privacy policy, set
+ * SDL_HINT_WINRT_PRIVACY_POLICY_URL before calling any SDL_Init() functions.
+ * The contents of the hint should be a valid URL. For example,
+ * "http://www.example.com".
+ *
+ * The default value is "", which will prevent SDL from adding a privacy
+ * policy link to the Settings charm. This hint should only be set during app
+ * init.
+ *
+ * The label text of an app's "Privacy Policy" link may be customized via
+ * another hint, SDL_HINT_WINRT_PRIVACY_POLICY_LABEL.
+ *
+ * Please note that on Windows Phone, Microsoft does not provide standard UI
+ * for displaying a privacy policy link, and as such,
+ * SDL_HINT_WINRT_PRIVACY_POLICY_URL will not get used on that platform.
+ * Network-enabled phone apps should display their privacy policy through some
+ * other, in-app means.
+ */
+#define SDL_HINT_WINRT_PRIVACY_POLICY_URL "SDL_WINRT_PRIVACY_POLICY_URL"
+
+/**
+ * Mark X11 windows as override-redirect.
+ *
+ * If set, this _might_ increase framerate at the expense of the desktop not
+ * working as expected. Override-redirect windows aren't noticed by the window
+ * manager at all.
+ *
+ * You should probably only use this for fullscreen windows, and you probably
+ * shouldn't even use it for that. But it's here if you want to try!
+ */
+#define SDL_HINT_X11_FORCE_OVERRIDE_REDIRECT "SDL_X11_FORCE_OVERRIDE_REDIRECT"
+
+/**
+ * A variable that lets you disable the detection and use of Xinput gamepad
+ * devices
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Disable XInput detection (only uses direct input)
+ * - "1": Enable XInput detection (the default)
+ */
+#define SDL_HINT_XINPUT_ENABLED "SDL_XINPUT_ENABLED"
+
+/**
+ * A variable that lets you disable the detection and use of DirectInput
+ * gamepad devices
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Disable DirectInput detection (only uses XInput)
+ * - "1": Enable DirectInput detection (the default)
+ */
+#define SDL_HINT_DIRECTINPUT_ENABLED "SDL_DIRECTINPUT_ENABLED"
+
+/**
+ * A variable that causes SDL to use the old axis and button mapping for
+ * XInput devices.
+ *
+ * This hint is for backwards compatibility only and will be removed in SDL
+ * 2.1
+ *
+ * The default value is "0". This hint must be set before SDL_Init()
+ */
+#define SDL_HINT_XINPUT_USE_OLD_JOYSTICK_MAPPING "SDL_XINPUT_USE_OLD_JOYSTICK_MAPPING"
+
+/**
+ * A variable that causes SDL to not ignore audio "monitors"
+ *
+ * This is currently only used for PulseAudio and ignored elsewhere.
+ *
+ * By default, SDL ignores audio devices that aren't associated with physical
+ * hardware. Changing this hint to "1" will expose anything SDL sees that
+ * appears to be an audio source or sink. This will add "devices" to the list
+ * that the user probably doesn't want or need, but it can be useful in
+ * scenarios where you want to hook up SDL to some sort of virtual device,
+ * etc.
+ *
+ * The default value is "0". This hint must be set before SDL_Init().
+ *
+ * This hint is available since SDL 2.0.16. Before then, virtual devices are
+ * always ignored.
+ */
+#define SDL_HINT_AUDIO_INCLUDE_MONITORS "SDL_AUDIO_INCLUDE_MONITORS"
+
+/**
+ * A variable that forces X11 windows to create as a custom type.
+ *
+ * This is currently only used for X11 and ignored elsewhere.
+ *
+ * During SDL_CreateWindow, SDL uses the _NET_WM_WINDOW_TYPE X11 property to
+ * report to the window manager the type of window it wants to create. This
+ * might be set to various things if SDL_WINDOW_TOOLTIP or
+ * SDL_WINDOW_POPUP_MENU, etc, were specified. For "normal" windows that
+ * haven't set a specific type, this hint can be used to specify a custom
+ * type. For example, a dock window might set this to
+ * "_NET_WM_WINDOW_TYPE_DOCK".
+ *
+ * If not set or set to "", this hint is ignored. This hint must be set before
+ * the SDL_CreateWindow() call that it is intended to affect.
+ *
+ * This hint is available since SDL 2.0.22.
+ */
+#define SDL_HINT_X11_WINDOW_TYPE "SDL_X11_WINDOW_TYPE"
+
+/**
+ * A variable that decides whether to send SDL_QUIT when closing the final
+ * window.
+ *
+ * By default, SDL sends an SDL_QUIT event when there is only one window and
+ * it receives an SDL_WINDOWEVENT_CLOSE event, under the assumption most apps
+ * would also take the loss of this window as a signal to terminate the
+ * program.
+ *
+ * However, it's not unreasonable in some cases to have the program continue
+ * to live on, perhaps to create new windows later.
+ *
+ * Changing this hint to "0" will cause SDL to not send an SDL_QUIT event when
+ * the final window is requesting to close. Note that in this case, there are
+ * still other legitimate reasons one might get an SDL_QUIT event: choosing
+ * "Quit" from the macOS menu bar, sending a SIGINT (ctrl-c) on Unix, etc.
+ *
+ * The default value is "1". This hint can be changed at any time.
+ *
+ * This hint is available since SDL 2.0.22. Before then, you always get an
+ * SDL_QUIT event when closing the final window.
+ */
+#define SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE "SDL_QUIT_ON_LAST_WINDOW_CLOSE"
+
+
+/**
+ * A variable that decides what video backend to use.
+ *
+ * By default, SDL will try all available video backends in a reasonable order
+ * until it finds one that can work, but this hint allows the app or user to
+ * force a specific target, such as "x11" if, say, you are on Wayland but want
+ * to try talking to the X server instead.
+ *
+ * This functionality has existed since SDL 2.0.0 (indeed, before that) but
+ * before 2.0.22 this was an environment variable only. In 2.0.22, it was
+ * upgraded to a full SDL hint, so you can set the environment variable as
+ * usual or programatically set the hint with SDL_SetHint, which won't
+ * propagate to child processes.
+ *
+ * The default value is unset, in which case SDL will try to figure out the
+ * best video backend on your behalf. This hint needs to be set before
+ * SDL_Init() is called to be useful.
+ *
+ * This hint is available since SDL 2.0.22. Before then, you could set the
+ * environment variable to get the same effect.
+ */
+#define SDL_HINT_VIDEODRIVER "SDL_VIDEODRIVER"
+
+/**
+ * A variable that decides what audio backend to use.
+ *
+ * By default, SDL will try all available audio backends in a reasonable order
+ * until it finds one that can work, but this hint allows the app or user to
+ * force a specific target, such as "alsa" if, say, you are on PulseAudio but
+ * want to try talking to the lower level instead.
+ *
+ * This functionality has existed since SDL 2.0.0 (indeed, before that) but
+ * before 2.0.22 this was an environment variable only. In 2.0.22, it was
+ * upgraded to a full SDL hint, so you can set the environment variable as
+ * usual or programatically set the hint with SDL_SetHint, which won't
+ * propagate to child processes.
+ *
+ * The default value is unset, in which case SDL will try to figure out the
+ * best audio backend on your behalf. This hint needs to be set before
+ * SDL_Init() is called to be useful.
+ *
+ * This hint is available since SDL 2.0.22. Before then, you could set the
+ * environment variable to get the same effect.
+ */
+#define SDL_HINT_AUDIODRIVER "SDL_AUDIODRIVER"
+
+/**
+ * A variable that decides what KMSDRM device to use.
+ *
+ * Internally, SDL might open something like "/dev/dri/cardNN" to access
+ * KMSDRM functionality, where "NN" is a device index number.
+ *
+ * SDL makes a guess at the best index to use (usually zero), but the app or
+ * user can set this hint to a number between 0 and 99 to force selection.
+ *
+ * This hint is available since SDL 2.24.0.
+ */
+#define SDL_HINT_KMSDRM_DEVICE_INDEX "SDL_KMSDRM_DEVICE_INDEX"
+
+
+/**
+ * A variable that treats trackpads as touch devices.
+ *
+ * On macOS (and possibly other platforms in the future), SDL will report
+ * touches on a trackpad as mouse input, which is generally what users expect
+ * from this device; however, these are often actually full multitouch-capable
+ * touch devices, so it might be preferable to some apps to treat them as
+ * such.
+ *
+ * Setting this hint to true will make the trackpad input report as a
+ * multitouch device instead of a mouse. The default is false.
+ *
+ * Note that most platforms don't support this hint. As of 2.24.0, it only
+ * supports MacBooks' trackpads on macOS. Others may follow later.
+ *
+ * This hint is checked during SDL_Init and can not be changed after.
+ *
+ * This hint is available since SDL 2.24.0.
+ */
+#define SDL_HINT_TRACKPAD_IS_TOUCH_ONLY "SDL_TRACKPAD_IS_TOUCH_ONLY"
+
+/**
+ * Cause SDL to call dbus_shutdown() on quit.
+ *
+ * This is useful as a debug tool to validate memory leaks, but shouldn't ever
+ * be set in production applications, as other libraries used by the
+ * application might use dbus under the hood and this cause cause crashes if
+ * they continue after SDL_Quit().
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": SDL will not call dbus_shutdown() on quit (default)
+ * - "1": SDL will call dbus_shutdown() on quit
+ *
+ * This hint is available since SDL 2.30.0.
+ */
+#define SDL_HINT_SHUTDOWN_DBUS_ON_QUIT "SDL_SHUTDOWN_DBUS_ON_QUIT"
+
+/**
+ * Specify if SDL_RWFromFile should use the resource dir on Apple platforms.
+ *
+ * SDL2 has always done this on Apple platforms, but it can be surprising to
+ * try opening a path to discover that SDL adjusts the path to elsewhere, so
+ * this hint allows that behavior to be disabled.
+ *
+ * If running from a App Bundle, this will be MyApp.app/Contents/Resources. If
+ * running as a normal Unix-like process, this will be the directory where the
+ * running binary lives. Setting this hint to 0 avoids this and just uses the
+ * requested path as-is.
+ *
+ * This variable can be set to the following values:
+ *
+ * - "0": SDL will not use the app resource directory.
+ * - "1": SDL will use the app's resource directory (default).
+ *
+ * This hint is available since SDL 2.32.0.
+ */
+#define SDL_HINT_APPLE_RWFROMFILE_USE_RESOURCES "SDL_APPLE_RWFROMFILE_USE_RESOURCES"
+
+
+/**
+ * An enumeration of hint priorities
+ */
+typedef enum SDL_HintPriority
+{
+ SDL_HINT_DEFAULT,
+ SDL_HINT_NORMAL,
+ SDL_HINT_OVERRIDE
+} SDL_HintPriority;
+
+
+/**
+ * Set a hint with a specific priority.
+ *
+ * The priority controls the behavior when setting a hint that already has a
+ * value. Hints will replace existing hints of their priority and lower.
+ * Environment variables are considered to have override priority.
+ *
+ * \param name the hint to set.
+ * \param value the value of the hint variable.
+ * \param priority the SDL_HintPriority level for the hint.
+ * \returns SDL_TRUE if the hint was set, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetHint
+ * \sa SDL_SetHint
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_SetHintWithPriority(const char *name,
+ const char *value,
+ SDL_HintPriority priority);
+
+/**
+ * Set a hint with normal priority.
+ *
+ * Hints will not be set if there is an existing override hint or environment
+ * variable that takes precedence. You can use SDL_SetHintWithPriority() to
+ * set the hint with override priority instead.
+ *
+ * \param name the hint to set.
+ * \param value the value of the hint variable.
+ * \returns SDL_TRUE if the hint was set, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetHint
+ * \sa SDL_SetHintWithPriority
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_SetHint(const char *name,
+ const char *value);
+
+/**
+ * Reset a hint to the default value.
+ *
+ * This will reset a hint to the value of the environment variable, or NULL if
+ * the environment isn't set. Callbacks will be called normally with this
+ * change.
+ *
+ * \param name the hint to set.
+ * \returns SDL_TRUE if the hint was set, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GetHint
+ * \sa SDL_SetHint
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_ResetHint(const char *name);
+
+/**
+ * Reset all hints to the default values.
+ *
+ * This will reset all hints to the value of the associated environment
+ * variable, or NULL if the environment isn't set. Callbacks will be called
+ * normally with this change.
+ *
+ * \since This function is available since SDL 2.26.0.
+ *
+ * \sa SDL_GetHint
+ * \sa SDL_SetHint
+ * \sa SDL_ResetHint
+ */
+extern DECLSPEC void SDLCALL SDL_ResetHints(void);
+
+/**
+ * Get the value of a hint.
+ *
+ * \param name the hint to query.
+ * \returns the string value of a hint or NULL if the hint isn't set.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_SetHint
+ * \sa SDL_SetHintWithPriority
+ */
+extern DECLSPEC const char * SDLCALL SDL_GetHint(const char *name);
+
+/**
+ * Get the boolean value of a hint variable.
+ *
+ * \param name the name of the hint to get the boolean value from.
+ * \param default_value the value to return if the hint does not exist.
+ * \returns the boolean value of a hint or the provided default value if the
+ * hint does not exist.
+ *
+ * \since This function is available since SDL 2.0.5.
+ *
+ * \sa SDL_GetHint
+ * \sa SDL_SetHint
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_GetHintBoolean(const char *name, SDL_bool default_value);
+
+/**
+ * Type definition of the hint callback function.
+ *
+ * \param userdata what was passed as `userdata` to SDL_AddHintCallback().
+ * \param name what was passed as `name` to SDL_AddHintCallback().
+ * \param oldValue the previous hint value.
+ * \param newValue the new value hint is to be set to.
+ */
+typedef void (SDLCALL *SDL_HintCallback)(void *userdata, const char *name, const char *oldValue, const char *newValue);
+
+/**
+ * Add a function to watch a particular hint.
+ *
+ * \param name the hint to watch.
+ * \param callback An SDL_HintCallback function that will be called when the
+ * hint value changes.
+ * \param userdata a pointer to pass to the callback function.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_DelHintCallback
+ */
+extern DECLSPEC void SDLCALL SDL_AddHintCallback(const char *name,
+ SDL_HintCallback callback,
+ void *userdata);
+
+/**
+ * Remove a function watching a particular hint.
+ *
+ * \param name the hint being watched.
+ * \param callback An SDL_HintCallback function that will be called when the
+ * hint value changes.
+ * \param userdata a pointer being passed to the callback function.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_AddHintCallback
+ */
+extern DECLSPEC void SDLCALL SDL_DelHintCallback(const char *name,
+ SDL_HintCallback callback,
+ void *userdata);
+
+/**
+ * Clear all hints.
+ *
+ * This function is automatically called during SDL_Quit(), and deletes all
+ * callbacks without calling them and frees all memory associated with hints.
+ * If you're calling this from application code you probably want to call
+ * SDL_ResetHints() instead.
+ *
+ * This function will be removed from the API the next time we rev the ABI.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_ResetHints
+ */
+extern DECLSPEC void SDLCALL SDL_ClearHints(void);
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_hints_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_joystick.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_joystick.h
new file mode 100644
index 00000000..668db5e3
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_joystick.h
@@ -0,0 +1,1088 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryJoystick
+ *
+ * Include file for SDL joystick event handling
+ *
+ * The term "device_index" identifies currently plugged in joystick devices
+ * between 0 and SDL_NumJoysticks(), with the exact joystick behind a
+ * device_index changing as joysticks are plugged and unplugged.
+ *
+ * The term "instance_id" is the current instantiation of a joystick device in
+ * the system, if the joystick is removed and then re-inserted then it will
+ * get a new instance_id, instance_id's are monotonically increasing
+ * identifiers of a joystick plugged in.
+ *
+ * The term "player_index" is the number assigned to a player on a specific
+ * controller. For XInput controllers this returns the XInput user index. Many
+ * joysticks will not be able to supply this information.
+ *
+ * The term JoystickGUID is a stable 128-bit identifier for a joystick device
+ * that does not change over time, it identifies class of the device (a X360
+ * wired controller for example). This identifier is platform dependent.
+ */
+
+#ifndef SDL_joystick_h_
+#define SDL_joystick_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+#include "SDL_guid.h"
+#include "SDL_mutex.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * \file SDL_joystick.h
+ *
+ * In order to use these functions, SDL_Init() must have been called
+ * with the SDL_INIT_JOYSTICK flag. This causes SDL to scan the system
+ * for joysticks, and load appropriate drivers.
+ *
+ * If you would like to receive joystick updates while the application
+ * is in the background, you should set the following hint before calling
+ * SDL_Init(): SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS
+ */
+
+/**
+ * The joystick structure used to identify an SDL joystick
+ */
+#ifdef SDL_THREAD_SAFETY_ANALYSIS
+extern SDL_mutex *SDL_joystick_lock;
+#endif
+struct _SDL_Joystick;
+typedef struct _SDL_Joystick SDL_Joystick;
+
+/**
+ * A structure that encodes the stable unique id for a joystick device.
+ *
+ * This is just a standard SDL_GUID by a different name.
+ */
+typedef SDL_GUID SDL_JoystickGUID;
+
+/**
+ * This is a unique ID for a joystick for the time it is connected to the
+ * system, and is never reused for the lifetime of the application.
+ *
+ * If the joystick is disconnected and reconnected, it will get a new ID.
+ *
+ * The ID value starts at 0 and increments from there. The value -1 is an
+ * invalid ID.
+ */
+typedef Sint32 SDL_JoystickID;
+
+typedef enum
+{
+ SDL_JOYSTICK_TYPE_UNKNOWN,
+ SDL_JOYSTICK_TYPE_GAMECONTROLLER,
+ SDL_JOYSTICK_TYPE_WHEEL,
+ SDL_JOYSTICK_TYPE_ARCADE_STICK,
+ SDL_JOYSTICK_TYPE_FLIGHT_STICK,
+ SDL_JOYSTICK_TYPE_DANCE_PAD,
+ SDL_JOYSTICK_TYPE_GUITAR,
+ SDL_JOYSTICK_TYPE_DRUM_KIT,
+ SDL_JOYSTICK_TYPE_ARCADE_PAD,
+ SDL_JOYSTICK_TYPE_THROTTLE
+} SDL_JoystickType;
+
+typedef enum
+{
+ SDL_JOYSTICK_POWER_UNKNOWN = -1,
+ SDL_JOYSTICK_POWER_EMPTY, /* <= 5% */
+ SDL_JOYSTICK_POWER_LOW, /* <= 20% */
+ SDL_JOYSTICK_POWER_MEDIUM, /* <= 70% */
+ SDL_JOYSTICK_POWER_FULL, /* <= 100% */
+ SDL_JOYSTICK_POWER_WIRED,
+ SDL_JOYSTICK_POWER_MAX
+} SDL_JoystickPowerLevel;
+
+/* Set max recognized G-force from accelerometer
+ See src/joystick/uikit/SDL_sysjoystick.m for notes on why this is needed
+ */
+#define SDL_IPHONE_MAX_GFORCE 5.0
+
+
+/* Function prototypes */
+
+/**
+ * Locking for multi-threaded access to the joystick API
+ *
+ * If you are using the joystick API or handling events from multiple threads
+ * you should use these locking functions to protect access to the joysticks.
+ *
+ * In particular, you are guaranteed that the joystick list won't change, so
+ * the API functions that take a joystick index will be valid, and joystick
+ * and game controller events will not be delivered.
+ *
+ * As of SDL 2.26.0, you can take the joystick lock around reinitializing the
+ * joystick subsystem, to prevent other threads from seeing joysticks in an
+ * uninitialized state. However, all open joysticks will be closed and SDL
+ * functions called with them will fail.
+ *
+ * \since This function is available since SDL 2.0.7.
+ */
+extern DECLSPEC void SDLCALL SDL_LockJoysticks(void) SDL_ACQUIRE(SDL_joystick_lock);
+
+
+/**
+ * Unlocking for multi-threaded access to the joystick API
+ *
+ * If you are using the joystick API or handling events from multiple threads
+ * you should use these locking functions to protect access to the joysticks.
+ *
+ * In particular, you are guaranteed that the joystick list won't change, so
+ * the API functions that take a joystick index will be valid, and joystick
+ * and game controller events will not be delivered.
+ *
+ * \since This function is available since SDL 2.0.7.
+ */
+extern DECLSPEC void SDLCALL SDL_UnlockJoysticks(void) SDL_RELEASE(SDL_joystick_lock);
+
+/**
+ * Count the number of joysticks attached to the system.
+ *
+ * \returns the number of attached joysticks on success or a negative error
+ * code on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickName
+ * \sa SDL_JoystickPath
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC int SDLCALL SDL_NumJoysticks(void);
+
+/**
+ * Get the implementation dependent name of a joystick.
+ *
+ * This can be called before any joysticks are opened.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system).
+ * \returns the name of the selected joystick. If no name can be found, this
+ * function returns NULL; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickName
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC const char *SDLCALL SDL_JoystickNameForIndex(int device_index);
+
+/**
+ * Get the implementation dependent path of a joystick.
+ *
+ * This can be called before any joysticks are opened.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system).
+ * \returns the path of the selected joystick. If no path can be found, this
+ * function returns NULL; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_JoystickPath
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC const char *SDLCALL SDL_JoystickPathForIndex(int device_index);
+
+/**
+ * Get the player index of a joystick, or -1 if it's not available This can be
+ * called before any joysticks are opened.
+ *
+ * \since This function is available since SDL 2.0.9.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickGetDevicePlayerIndex(int device_index);
+
+/**
+ * Get the implementation-dependent GUID for the joystick at a given device
+ * index.
+ *
+ * This function can be called before any joysticks are opened.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system.
+ * \returns the GUID of the selected joystick. If called on an invalid index,
+ * this function returns a zero GUID.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetGUID
+ * \sa SDL_JoystickGetGUIDString
+ */
+extern DECLSPEC SDL_JoystickGUID SDLCALL SDL_JoystickGetDeviceGUID(int device_index);
+
+/**
+ * Get the USB vendor ID of a joystick, if available.
+ *
+ * This can be called before any joysticks are opened. If the vendor ID isn't
+ * available this function returns 0.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system.
+ * \returns the USB vendor ID of the selected joystick. If called on an
+ * invalid index, this function returns zero.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_JoystickGetDeviceVendor(int device_index);
+
+/**
+ * Get the USB product ID of a joystick, if available.
+ *
+ * This can be called before any joysticks are opened. If the product ID isn't
+ * available this function returns 0.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system.
+ * \returns the USB product ID of the selected joystick. If called on an
+ * invalid index, this function returns zero.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_JoystickGetDeviceProduct(int device_index);
+
+/**
+ * Get the product version of a joystick, if available.
+ *
+ * This can be called before any joysticks are opened. If the product version
+ * isn't available this function returns 0.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system.
+ * \returns the product version of the selected joystick. If called on an
+ * invalid index, this function returns zero.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_JoystickGetDeviceProductVersion(int device_index);
+
+/**
+ * Get the type of a joystick, if available.
+ *
+ * This can be called before any joysticks are opened.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system.
+ * \returns the SDL_JoystickType of the selected joystick. If called on an
+ * invalid index, this function returns `SDL_JOYSTICK_TYPE_UNKNOWN`.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC SDL_JoystickType SDLCALL SDL_JoystickGetDeviceType(int device_index);
+
+/**
+ * Get the instance ID of a joystick.
+ *
+ * This can be called before any joysticks are opened.
+ *
+ * \param device_index the index of the joystick to query (the N'th joystick
+ * on the system.
+ * \returns the instance id of the selected joystick. If called on an invalid
+ * index, this function returns -1.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC SDL_JoystickID SDLCALL SDL_JoystickGetDeviceInstanceID(int device_index);
+
+/**
+ * Open a joystick for use.
+ *
+ * The `device_index` argument refers to the N'th joystick presently
+ * recognized by SDL on the system. It is **NOT** the same as the instance ID
+ * used to identify the joystick in future events. See
+ * SDL_JoystickInstanceID() for more details about instance IDs.
+ *
+ * The joystick subsystem must be initialized before a joystick can be opened
+ * for use.
+ *
+ * \param device_index the index of the joystick to query.
+ * \returns a joystick identifier or NULL if an error occurred; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickClose
+ * \sa SDL_JoystickInstanceID
+ */
+extern DECLSPEC SDL_Joystick *SDLCALL SDL_JoystickOpen(int device_index);
+
+/**
+ * Get the SDL_Joystick associated with an instance id.
+ *
+ * \param instance_id the instance id to get the SDL_Joystick for.
+ * \returns an SDL_Joystick on success or NULL on failure; call SDL_GetError()
+ * for more information.
+ *
+ * \since This function is available since SDL 2.0.4.
+ */
+extern DECLSPEC SDL_Joystick *SDLCALL SDL_JoystickFromInstanceID(SDL_JoystickID instance_id);
+
+/**
+ * Get the SDL_Joystick associated with a player index.
+ *
+ * \param player_index the player index to get the SDL_Joystick for.
+ * \returns an SDL_Joystick on success or NULL on failure; call SDL_GetError()
+ * for more information.
+ *
+ * \since This function is available since SDL 2.0.12.
+ */
+extern DECLSPEC SDL_Joystick *SDLCALL SDL_JoystickFromPlayerIndex(int player_index);
+
+/**
+ * Attach a new virtual joystick.
+ *
+ * \returns the joystick's device index, or -1 if an error occurred.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickAttachVirtual(SDL_JoystickType type,
+ int naxes,
+ int nbuttons,
+ int nhats);
+
+/**
+ * The structure that defines an extended virtual joystick description
+ *
+ * The caller must zero the structure and then initialize the version with
+ * `SDL_VIRTUAL_JOYSTICK_DESC_VERSION` before passing it to
+ * SDL_JoystickAttachVirtualEx() All other elements of this structure are
+ * optional and can be left 0.
+ *
+ * \sa SDL_JoystickAttachVirtualEx
+ */
+typedef struct SDL_VirtualJoystickDesc
+{
+ Uint16 version; /**< `SDL_VIRTUAL_JOYSTICK_DESC_VERSION` */
+ Uint16 type; /**< `SDL_JoystickType` */
+ Uint16 naxes; /**< the number of axes on this joystick */
+ Uint16 nbuttons; /**< the number of buttons on this joystick */
+ Uint16 nhats; /**< the number of hats on this joystick */
+ Uint16 vendor_id; /**< the USB vendor ID of this joystick */
+ Uint16 product_id; /**< the USB product ID of this joystick */
+ Uint16 padding; /**< unused */
+ Uint32 button_mask; /**< A mask of which buttons are valid for this controller
+ e.g. (1 << SDL_CONTROLLER_BUTTON_A) */
+ Uint32 axis_mask; /**< A mask of which axes are valid for this controller
+ e.g. (1 << SDL_CONTROLLER_AXIS_LEFTX) */
+ const char *name; /**< the name of the joystick */
+
+ void *userdata; /**< User data pointer passed to callbacks */
+ void (SDLCALL *Update)(void *userdata); /**< Called when the joystick state should be updated */
+ void (SDLCALL *SetPlayerIndex)(void *userdata, int player_index); /**< Called when the player index is set */
+ int (SDLCALL *Rumble)(void *userdata, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble); /**< Implements SDL_JoystickRumble() */
+ int (SDLCALL *RumbleTriggers)(void *userdata, Uint16 left_rumble, Uint16 right_rumble); /**< Implements SDL_JoystickRumbleTriggers() */
+ int (SDLCALL *SetLED)(void *userdata, Uint8 red, Uint8 green, Uint8 blue); /**< Implements SDL_JoystickSetLED() */
+ int (SDLCALL *SendEffect)(void *userdata, const void *data, int size); /**< Implements SDL_JoystickSendEffect() */
+
+} SDL_VirtualJoystickDesc;
+
+/**
+ * The current version of the SDL_VirtualJoystickDesc structure
+ */
+#define SDL_VIRTUAL_JOYSTICK_DESC_VERSION 1
+
+/**
+ * Attach a new virtual joystick with extended properties.
+ *
+ * \returns the joystick's device index, or -1 if an error occurred.
+ *
+ * \since This function is available since SDL 2.24.0.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickAttachVirtualEx(const SDL_VirtualJoystickDesc *desc);
+
+/**
+ * Detach a virtual joystick.
+ *
+ * \param device_index a value previously returned from
+ * SDL_JoystickAttachVirtual().
+ * \returns 0 on success, or -1 if an error occurred.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickDetachVirtual(int device_index);
+
+/**
+ * Query whether or not the joystick at a given device index is virtual.
+ *
+ * \param device_index a joystick device index.
+ * \returns SDL_TRUE if the joystick is virtual, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_JoystickIsVirtual(int device_index);
+
+/**
+ * Set values on an opened, virtual-joystick's axis.
+ *
+ * Please note that values set here will not be applied until the next call to
+ * SDL_JoystickUpdate, which can either be called directly, or can be called
+ * indirectly through various other SDL APIs, including, but not limited to
+ * the following: SDL_PollEvent, SDL_PumpEvents, SDL_WaitEventTimeout,
+ * SDL_WaitEvent.
+ *
+ * Note that when sending trigger axes, you should scale the value to the full
+ * range of Sint16. For example, a trigger at rest would have the value of
+ * `SDL_JOYSTICK_AXIS_MIN`.
+ *
+ * \param joystick the virtual joystick on which to set state.
+ * \param axis the specific axis on the virtual joystick to set.
+ * \param value the new value for the specified axis.
+ * \returns 0 on success, -1 on error.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualAxis(SDL_Joystick *joystick, int axis, Sint16 value);
+
+/**
+ * Set values on an opened, virtual-joystick's button.
+ *
+ * Please note that values set here will not be applied until the next call to
+ * SDL_JoystickUpdate, which can either be called directly, or can be called
+ * indirectly through various other SDL APIs, including, but not limited to
+ * the following: SDL_PollEvent, SDL_PumpEvents, SDL_WaitEventTimeout,
+ * SDL_WaitEvent.
+ *
+ * \param joystick the virtual joystick on which to set state.
+ * \param button the specific button on the virtual joystick to set.
+ * \param value the new value for the specified button.
+ * \returns 0 on success, -1 on error.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualButton(SDL_Joystick *joystick, int button, Uint8 value);
+
+/**
+ * Set values on an opened, virtual-joystick's hat.
+ *
+ * Please note that values set here will not be applied until the next call to
+ * SDL_JoystickUpdate, which can either be called directly, or can be called
+ * indirectly through various other SDL APIs, including, but not limited to
+ * the following: SDL_PollEvent, SDL_PumpEvents, SDL_WaitEventTimeout,
+ * SDL_WaitEvent.
+ *
+ * \param joystick the virtual joystick on which to set state.
+ * \param hat the specific hat on the virtual joystick to set.
+ * \param value the new value for the specified hat.
+ * \returns 0 on success, -1 on error.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualHat(SDL_Joystick *joystick, int hat, Uint8 value);
+
+/**
+ * Get the implementation dependent name of a joystick.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the name of the selected joystick. If no name can be found, this
+ * function returns NULL; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickNameForIndex
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC const char *SDLCALL SDL_JoystickName(SDL_Joystick *joystick);
+
+/**
+ * Get the implementation dependent path of a joystick.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the path of the selected joystick. If no path can be found, this
+ * function returns NULL; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_JoystickPathForIndex
+ */
+extern DECLSPEC const char *SDLCALL SDL_JoystickPath(SDL_Joystick *joystick);
+
+/**
+ * Get the player index of an opened joystick.
+ *
+ * For XInput controllers this returns the XInput user index. Many joysticks
+ * will not be able to supply this information.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the player index, or -1 if it's not available.
+ *
+ * \since This function is available since SDL 2.0.9.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickGetPlayerIndex(SDL_Joystick *joystick);
+
+/**
+ * Set the player index of an opened joystick.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \param player_index Player index to assign to this joystick, or -1 to clear
+ * the player index and turn off player LEDs.
+ *
+ * \since This function is available since SDL 2.0.12.
+ */
+extern DECLSPEC void SDLCALL SDL_JoystickSetPlayerIndex(SDL_Joystick *joystick, int player_index);
+
+/**
+ * Get the implementation-dependent GUID for the joystick.
+ *
+ * This function requires an open joystick.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the GUID of the given joystick. If called on an invalid index,
+ * this function returns a zero GUID; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetDeviceGUID
+ * \sa SDL_JoystickGetGUIDString
+ */
+extern DECLSPEC SDL_JoystickGUID SDLCALL SDL_JoystickGetGUID(SDL_Joystick *joystick);
+
+/**
+ * Get the USB vendor ID of an opened joystick, if available.
+ *
+ * If the vendor ID isn't available this function returns 0.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the USB vendor ID of the selected joystick, or 0 if unavailable.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_JoystickGetVendor(SDL_Joystick *joystick);
+
+/**
+ * Get the USB product ID of an opened joystick, if available.
+ *
+ * If the product ID isn't available this function returns 0.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the USB product ID of the selected joystick, or 0 if unavailable.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_JoystickGetProduct(SDL_Joystick *joystick);
+
+/**
+ * Get the product version of an opened joystick, if available.
+ *
+ * If the product version isn't available this function returns 0.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the product version of the selected joystick, or 0 if unavailable.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_JoystickGetProductVersion(SDL_Joystick *joystick);
+
+/**
+ * Get the firmware version of an opened joystick, if available.
+ *
+ * If the firmware version isn't available this function returns 0.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the firmware version of the selected joystick, or 0 if
+ * unavailable.
+ *
+ * \since This function is available since SDL 2.24.0.
+ */
+extern DECLSPEC Uint16 SDLCALL SDL_JoystickGetFirmwareVersion(SDL_Joystick *joystick);
+
+/**
+ * Get the serial number of an opened joystick, if available.
+ *
+ * Returns the serial number of the joystick, or NULL if it is not available.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the serial number of the selected joystick, or NULL if
+ * unavailable.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC const char * SDLCALL SDL_JoystickGetSerial(SDL_Joystick *joystick);
+
+/**
+ * Get the type of an opened joystick.
+ *
+ * \param joystick the SDL_Joystick obtained from SDL_JoystickOpen().
+ * \returns the SDL_JoystickType of the selected joystick.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC SDL_JoystickType SDLCALL SDL_JoystickGetType(SDL_Joystick *joystick);
+
+/**
+ * Get an ASCII string representation for a given SDL_JoystickGUID.
+ *
+ * You should supply at least 33 bytes for pszGUID.
+ *
+ * \param guid the SDL_JoystickGUID you wish to convert to string.
+ * \param pszGUID buffer in which to write the ASCII string.
+ * \param cbGUID the size of pszGUID.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetDeviceGUID
+ * \sa SDL_JoystickGetGUID
+ * \sa SDL_JoystickGetGUIDFromString
+ */
+extern DECLSPEC void SDLCALL SDL_JoystickGetGUIDString(SDL_JoystickGUID guid, char *pszGUID, int cbGUID);
+
+/**
+ * Convert a GUID string into a SDL_JoystickGUID structure.
+ *
+ * Performs no error checking. If this function is given a string containing
+ * an invalid GUID, the function will silently succeed, but the GUID generated
+ * will not be useful.
+ *
+ * \param pchGUID string containing an ASCII representation of a GUID.
+ * \returns a SDL_JoystickGUID structure.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetGUIDString
+ */
+extern DECLSPEC SDL_JoystickGUID SDLCALL SDL_JoystickGetGUIDFromString(const char *pchGUID);
+
+/**
+ * Get the device information encoded in a SDL_JoystickGUID structure
+ *
+ * \param guid the SDL_JoystickGUID you wish to get info about.
+ * \param vendor A pointer filled in with the device VID, or 0 if not
+ * available.
+ * \param product A pointer filled in with the device PID, or 0 if not
+ * available.
+ * \param version A pointer filled in with the device version, or 0 if not
+ * available.
+ * \param crc16 A pointer filled in with a CRC used to distinguish different
+ * products with the same VID/PID, or 0 if not available.
+ *
+ * \since This function is available since SDL 2.26.0.
+ *
+ * \sa SDL_JoystickGetDeviceGUID
+ */
+extern DECLSPEC void SDLCALL SDL_GetJoystickGUIDInfo(SDL_JoystickGUID guid, Uint16 *vendor, Uint16 *product, Uint16 *version, Uint16 *crc16);
+
+/**
+ * Get the status of a specified joystick.
+ *
+ * \param joystick the joystick to query.
+ * \returns SDL_TRUE if the joystick has been opened, SDL_FALSE if it has not;
+ * call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickClose
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_JoystickGetAttached(SDL_Joystick *joystick);
+
+/**
+ * Get the instance ID of an opened joystick.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \returns the instance ID of the specified joystick on success or a negative
+ * error code on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC SDL_JoystickID SDLCALL SDL_JoystickInstanceID(SDL_Joystick *joystick);
+
+/**
+ * Get the number of general axis controls on a joystick.
+ *
+ * Often, the directional pad on a game controller will either look like 4
+ * separate buttons or a POV hat, and not axes, but all of this is up to the
+ * device and platform.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \returns the number of axis controls/number of axes on success or a
+ * negative error code on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetAxis
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickNumAxes(SDL_Joystick *joystick);
+
+/**
+ * Get the number of trackballs on a joystick.
+ *
+ * Joystick trackballs have only relative motion events associated with them
+ * and their state cannot be polled.
+ *
+ * Most joysticks do not have trackballs.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \returns the number of trackballs on success or a negative error code on
+ * failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetBall
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickNumBalls(SDL_Joystick *joystick);
+
+/**
+ * Get the number of POV hats on a joystick.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \returns the number of POV hats on success or a negative error code on
+ * failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetHat
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickNumHats(SDL_Joystick *joystick);
+
+/**
+ * Get the number of buttons on a joystick.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \returns the number of buttons on success or a negative error code on
+ * failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickGetButton
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickNumButtons(SDL_Joystick *joystick);
+
+/**
+ * Update the current state of the open joysticks.
+ *
+ * This is called automatically by the event loop if any joystick events are
+ * enabled.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickEventState
+ */
+extern DECLSPEC void SDLCALL SDL_JoystickUpdate(void);
+
+/**
+ * Enable/disable joystick event polling.
+ *
+ * If joystick events are disabled, you must call SDL_JoystickUpdate()
+ * yourself and manually check the state of the joystick when you want
+ * joystick information.
+ *
+ * It is recommended that you leave joystick event handling enabled.
+ *
+ * **WARNING**: Calling this function may delete all events currently in SDL's
+ * event queue.
+ *
+ * While `param` is meant to be one of `SDL_QUERY`, `SDL_IGNORE`, or
+ * `SDL_ENABLE`, this function accepts any value, with any non-zero value that
+ * isn't `SDL_QUERY` being treated as `SDL_ENABLE`.
+ *
+ * If SDL was built with events disabled (extremely uncommon!), this will do
+ * nothing and always return `SDL_IGNORE`.
+ *
+ * \param state can be one of `SDL_QUERY`, `SDL_IGNORE`, or `SDL_ENABLE`.
+ * \returns If `state` is `SDL_QUERY` then the current state is returned,
+ * otherwise `state` is returned (even if it was not one of the
+ * allowed values).
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GameControllerEventState
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickEventState(int state);
+
+/* Limits for joystick axes... */
+#define SDL_JOYSTICK_AXIS_MAX 32767
+#define SDL_JOYSTICK_AXIS_MIN -32768
+
+/**
+ * Get the current state of an axis control on a joystick.
+ *
+ * SDL makes no promises about what part of the joystick any given axis refers
+ * to. Your game should have some sort of configuration UI to let users
+ * specify what each axis should be bound to. Alternately, SDL's higher-level
+ * Game Controller API makes a great effort to apply order to this lower-level
+ * interface, so you know that a specific axis is the "left thumb stick," etc.
+ *
+ * The value returned by SDL_JoystickGetAxis() is a signed integer (-32768 to
+ * 32767) representing the current position of the axis. It may be necessary
+ * to impose certain tolerances on these values to account for jitter.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \param axis the axis to query; the axis indices start at index 0.
+ * \returns a 16-bit signed integer representing the current position of the
+ * axis or 0 on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickNumAxes
+ */
+extern DECLSPEC Sint16 SDLCALL SDL_JoystickGetAxis(SDL_Joystick *joystick,
+ int axis);
+
+/**
+ * Get the initial state of an axis control on a joystick.
+ *
+ * The state is a value ranging from -32768 to 32767.
+ *
+ * The axis indices start at index 0.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \param axis the axis to query; the axis indices start at index 0.
+ * \param state Upon return, the initial value is supplied here.
+ * \return SDL_TRUE if this axis has any initial value, or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.6.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_JoystickGetAxisInitialState(SDL_Joystick *joystick,
+ int axis, Sint16 *state);
+
+/**
+ * \name Hat positions
+ */
+/* @{ */
+#define SDL_HAT_CENTERED 0x00
+#define SDL_HAT_UP 0x01
+#define SDL_HAT_RIGHT 0x02
+#define SDL_HAT_DOWN 0x04
+#define SDL_HAT_LEFT 0x08
+#define SDL_HAT_RIGHTUP (SDL_HAT_RIGHT|SDL_HAT_UP)
+#define SDL_HAT_RIGHTDOWN (SDL_HAT_RIGHT|SDL_HAT_DOWN)
+#define SDL_HAT_LEFTUP (SDL_HAT_LEFT|SDL_HAT_UP)
+#define SDL_HAT_LEFTDOWN (SDL_HAT_LEFT|SDL_HAT_DOWN)
+/* @} */
+
+/**
+ * Get the current state of a POV hat on a joystick.
+ *
+ * The returned value will be one of the following positions:
+ *
+ * - `SDL_HAT_CENTERED`
+ * - `SDL_HAT_UP`
+ * - `SDL_HAT_RIGHT`
+ * - `SDL_HAT_DOWN`
+ * - `SDL_HAT_LEFT`
+ * - `SDL_HAT_RIGHTUP`
+ * - `SDL_HAT_RIGHTDOWN`
+ * - `SDL_HAT_LEFTUP`
+ * - `SDL_HAT_LEFTDOWN`
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \param hat the hat index to get the state from; indices start at index 0.
+ * \returns the current hat position.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickNumHats
+ */
+extern DECLSPEC Uint8 SDLCALL SDL_JoystickGetHat(SDL_Joystick *joystick,
+ int hat);
+
+/**
+ * Get the ball axis change since the last poll.
+ *
+ * Trackballs can only return relative motion since the last call to
+ * SDL_JoystickGetBall(), these motion deltas are placed into `dx` and `dy`.
+ *
+ * Most joysticks do not have trackballs.
+ *
+ * \param joystick the SDL_Joystick to query.
+ * \param ball the ball index to query; ball indices start at index 0.
+ * \param dx stores the difference in the x axis position since the last poll.
+ * \param dy stores the difference in the y axis position since the last poll.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickNumBalls
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickGetBall(SDL_Joystick *joystick,
+ int ball, int *dx, int *dy);
+
+/**
+ * Get the current state of a button on a joystick.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information.
+ * \param button the button index to get the state from; indices start at
+ * index 0.
+ * \returns 1 if the specified button is pressed, 0 otherwise.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickNumButtons
+ */
+extern DECLSPEC Uint8 SDLCALL SDL_JoystickGetButton(SDL_Joystick *joystick,
+ int button);
+
+/**
+ * Start a rumble effect.
+ *
+ * Each call to this function cancels any previous rumble effect, and calling
+ * it with 0 intensity stops any rumbling.
+ *
+ * \param joystick The joystick to vibrate.
+ * \param low_frequency_rumble The intensity of the low frequency (left)
+ * rumble motor, from 0 to 0xFFFF.
+ * \param high_frequency_rumble The intensity of the high frequency (right)
+ * rumble motor, from 0 to 0xFFFF.
+ * \param duration_ms The duration of the rumble effect, in milliseconds.
+ * \returns 0, or -1 if rumble isn't supported on this joystick.
+ *
+ * \since This function is available since SDL 2.0.9.
+ *
+ * \sa SDL_JoystickHasRumble
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble, Uint32 duration_ms);
+
+/**
+ * Start a rumble effect in the joystick's triggers
+ *
+ * Each call to this function cancels any previous trigger rumble effect, and
+ * calling it with 0 intensity stops any rumbling.
+ *
+ * Note that this is rumbling of the _triggers_ and not the game controller as
+ * a whole. This is currently only supported on Xbox One controllers. If you
+ * want the (more common) whole-controller rumble, use SDL_JoystickRumble()
+ * instead.
+ *
+ * \param joystick The joystick to vibrate.
+ * \param left_rumble The intensity of the left trigger rumble motor, from 0
+ * to 0xFFFF.
+ * \param right_rumble The intensity of the right trigger rumble motor, from 0
+ * to 0xFFFF.
+ * \param duration_ms The duration of the rumble effect, in milliseconds.
+ * \returns 0, or -1 if trigger rumble isn't supported on this joystick.
+ *
+ * \since This function is available since SDL 2.0.14.
+ *
+ * \sa SDL_JoystickHasRumbleTriggers
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble, Uint32 duration_ms);
+
+/**
+ * Query whether a joystick has an LED.
+ *
+ * An example of a joystick LED is the light on the back of a PlayStation 4's
+ * DualShock 4 controller.
+ *
+ * \param joystick The joystick to query.
+ * \return SDL_TRUE if the joystick has a modifiable LED, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_JoystickHasLED(SDL_Joystick *joystick);
+
+/**
+ * Query whether a joystick has rumble support.
+ *
+ * \param joystick The joystick to query.
+ * \return SDL_TRUE if the joystick has rumble, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_JoystickRumble
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_JoystickHasRumble(SDL_Joystick *joystick);
+
+/**
+ * Query whether a joystick has rumble support on triggers.
+ *
+ * \param joystick The joystick to query.
+ * \return SDL_TRUE if the joystick has trigger rumble, SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 2.0.18.
+ *
+ * \sa SDL_JoystickRumbleTriggers
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_JoystickHasRumbleTriggers(SDL_Joystick *joystick);
+
+/**
+ * Update a joystick's LED color.
+ *
+ * An example of a joystick LED is the light on the back of a PlayStation 4's
+ * DualShock 4 controller.
+ *
+ * \param joystick The joystick to update.
+ * \param red The intensity of the red LED.
+ * \param green The intensity of the green LED.
+ * \param blue The intensity of the blue LED.
+ * \returns 0 on success, -1 if this joystick does not have a modifiable LED.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue);
+
+/**
+ * Send a joystick specific effect packet
+ *
+ * \param joystick The joystick to affect.
+ * \param data The data to send to the joystick.
+ * \param size The size of the data to send to the joystick.
+ * \returns 0, or -1 if this joystick or driver doesn't support effect
+ * packets.
+ *
+ * \since This function is available since SDL 2.0.16.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size);
+
+/**
+ * Close a joystick previously opened with SDL_JoystickOpen().
+ *
+ * \param joystick The joystick device to close.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_JoystickOpen
+ */
+extern DECLSPEC void SDLCALL SDL_JoystickClose(SDL_Joystick *joystick);
+
+/**
+ * Get the battery level of a joystick as SDL_JoystickPowerLevel.
+ *
+ * \param joystick the SDL_Joystick to query.
+ * \returns the current battery level as SDL_JoystickPowerLevel on success or
+ * `SDL_JOYSTICK_POWER_UNKNOWN` if it is unknown.
+ *
+ * \since This function is available since SDL 2.0.4.
+ */
+extern DECLSPEC SDL_JoystickPowerLevel SDLCALL SDL_JoystickCurrentPowerLevel(SDL_Joystick *joystick);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_joystick_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_keyboard.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_keyboard.h
new file mode 100644
index 00000000..eb46db52
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_keyboard.h
@@ -0,0 +1,361 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryKeyboard
+ *
+ * Include file for SDL keyboard event handling
+ */
+
+#ifndef SDL_keyboard_h_
+#define SDL_keyboard_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+#include "SDL_keycode.h"
+#include "SDL_video.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The SDL keysym structure, used in key events.
+ *
+ * If you are looking for translated character input, see the SDL_TEXTINPUT
+ * event.
+ */
+typedef struct SDL_Keysym
+{
+ SDL_Scancode scancode; /**< SDL physical key code - see SDL_Scancode for details */
+ SDL_Keycode sym; /**< SDL virtual key code - see SDL_Keycode for details */
+ Uint16 mod; /**< current key modifiers - see SDL_Keymod for details */
+ Uint32 unused;
+} SDL_Keysym;
+
+/* Function prototypes */
+
+/**
+ * Query the window which currently has keyboard focus.
+ *
+ * \returns the window with keyboard focus.
+ *
+ * \since This function is available since SDL 2.0.0.
+ */
+extern DECLSPEC SDL_Window * SDLCALL SDL_GetKeyboardFocus(void);
+
+/**
+ * Get a snapshot of the current state of the keyboard.
+ *
+ * The pointer returned is a pointer to an internal SDL array. It will be
+ * valid for the whole lifetime of the application and should not be freed by
+ * the caller.
+ *
+ * A array element with a value of 1 means that the key is pressed and a value
+ * of 0 means that it is not. Indexes into this array are obtained by using
+ * SDL_Scancode values.
+ *
+ * Use SDL_PumpEvents() to update the state array.
+ *
+ * This function gives you the current state after all events have been
+ * processed, so if a key or button has been pressed and released before you
+ * process events, then the pressed state will never show up in the
+ * SDL_GetKeyboardState() calls.
+ *
+ * Note: This function doesn't take into account whether shift has been
+ * pressed or not.
+ *
+ * \param numkeys if non-NULL, receives the length of the returned array.
+ * \returns a pointer to an array of key states.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_PumpEvents
+ * \sa SDL_ResetKeyboard
+ */
+extern DECLSPEC const Uint8 *SDLCALL SDL_GetKeyboardState(int *numkeys);
+
+/**
+ * Clear the state of the keyboard
+ *
+ * This function will generate key up events for all pressed keys.
+ *
+ * \since This function is available since SDL 2.24.0.
+ *
+ * \sa SDL_GetKeyboardState
+ */
+extern DECLSPEC void SDLCALL SDL_ResetKeyboard(void);
+
+/**
+ * Get the current key modifier state for the keyboard.
+ *
+ * \returns an OR'd combination of the modifier keys for the keyboard. See
+ * SDL_Keymod for details.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetKeyboardState
+ * \sa SDL_SetModState
+ */
+extern DECLSPEC SDL_Keymod SDLCALL SDL_GetModState(void);
+
+/**
+ * Set the current key modifier state for the keyboard.
+ *
+ * The inverse of SDL_GetModState(), SDL_SetModState() allows you to impose
+ * modifier key states on your application. Simply pass your desired modifier
+ * states into `modstate`. This value may be a bitwise, OR'd combination of
+ * SDL_Keymod values.
+ *
+ * This does not change the keyboard state, only the key modifier flags that
+ * SDL reports.
+ *
+ * \param modstate the desired SDL_Keymod for the keyboard.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetModState
+ */
+extern DECLSPEC void SDLCALL SDL_SetModState(SDL_Keymod modstate);
+
+/**
+ * Get the key code corresponding to the given scancode according to the
+ * current keyboard layout.
+ *
+ * See SDL_Keycode for details.
+ *
+ * \param scancode the desired SDL_Scancode to query.
+ * \returns the SDL_Keycode that corresponds to the given SDL_Scancode.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetKeyName
+ * \sa SDL_GetScancodeFromKey
+ */
+extern DECLSPEC SDL_Keycode SDLCALL SDL_GetKeyFromScancode(SDL_Scancode scancode);
+
+/**
+ * Get the scancode corresponding to the given key code according to the
+ * current keyboard layout.
+ *
+ * See SDL_Scancode for details.
+ *
+ * \param key the desired SDL_Keycode to query.
+ * \returns the SDL_Scancode that corresponds to the given SDL_Keycode.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetKeyFromScancode
+ * \sa SDL_GetScancodeName
+ */
+extern DECLSPEC SDL_Scancode SDLCALL SDL_GetScancodeFromKey(SDL_Keycode key);
+
+/**
+ * Get a human-readable name for a scancode.
+ *
+ * See SDL_Scancode for details.
+ *
+ * **Warning**: The returned name is by design not stable across platforms,
+ * e.g. the name for `SDL_SCANCODE_LGUI` is "Left GUI" under Linux but "Left
+ * Windows" under Microsoft Windows, and some scancodes like
+ * `SDL_SCANCODE_NONUSBACKSLASH` don't have any name at all. There are even
+ * scancodes that share names, e.g. `SDL_SCANCODE_RETURN` and
+ * `SDL_SCANCODE_RETURN2` (both called "Return"). This function is therefore
+ * unsuitable for creating a stable cross-platform two-way mapping between
+ * strings and scancodes.
+ *
+ * \param scancode the desired SDL_Scancode to query.
+ * \returns a pointer to the name for the scancode. If the scancode doesn't
+ * have a name this function returns an empty string ("").
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetScancodeFromKey
+ * \sa SDL_GetScancodeFromName
+ */
+extern DECLSPEC const char *SDLCALL SDL_GetScancodeName(SDL_Scancode scancode);
+
+/**
+ * Get a scancode from a human-readable name.
+ *
+ * \param name the human-readable scancode name.
+ * \returns the SDL_Scancode, or `SDL_SCANCODE_UNKNOWN` if the name wasn't
+ * recognized; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetKeyFromName
+ * \sa SDL_GetScancodeFromKey
+ * \sa SDL_GetScancodeName
+ */
+extern DECLSPEC SDL_Scancode SDLCALL SDL_GetScancodeFromName(const char *name);
+
+/**
+ * Get a human-readable name for a key.
+ *
+ * See SDL_Scancode and SDL_Keycode for details.
+ *
+ * \param key the desired SDL_Keycode to query.
+ * \returns a pointer to a UTF-8 string that stays valid at least until the
+ * next call to this function. If you need it around any longer, you
+ * must copy it. If the key doesn't have a name, this function
+ * returns an empty string ("").
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetKeyFromName
+ * \sa SDL_GetKeyFromScancode
+ * \sa SDL_GetScancodeFromKey
+ */
+extern DECLSPEC const char *SDLCALL SDL_GetKeyName(SDL_Keycode key);
+
+/**
+ * Get a key code from a human-readable name.
+ *
+ * \param name the human-readable key name.
+ * \returns key code, or `SDLK_UNKNOWN` if the name wasn't recognized; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_GetKeyFromScancode
+ * \sa SDL_GetKeyName
+ * \sa SDL_GetScancodeFromName
+ */
+extern DECLSPEC SDL_Keycode SDLCALL SDL_GetKeyFromName(const char *name);
+
+/**
+ * Start accepting Unicode text input events.
+ *
+ * This function will start accepting Unicode text input events in the focused
+ * SDL window, and start emitting SDL_TextInputEvent (SDL_TEXTINPUT) and
+ * SDL_TextEditingEvent (SDL_TEXTEDITING) events. Please use this function in
+ * pair with SDL_StopTextInput().
+ *
+ * On some platforms using this function activates the screen keyboard.
+ *
+ * On desktop platforms, SDL_StartTextInput() is implicitly called on SDL
+ * video subsystem initialization which will cause SDL_TextInputEvent and
+ * SDL_TextEditingEvent to begin emitting.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_SetTextInputRect
+ * \sa SDL_StopTextInput
+ */
+extern DECLSPEC void SDLCALL SDL_StartTextInput(void);
+
+/**
+ * Check whether or not Unicode text input events are enabled.
+ *
+ * \returns SDL_TRUE if text input events are enabled else SDL_FALSE.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_StartTextInput
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_IsTextInputActive(void);
+
+/**
+ * Stop receiving any text input events.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_StartTextInput
+ */
+extern DECLSPEC void SDLCALL SDL_StopTextInput(void);
+
+/**
+ * Dismiss the composition window/IME without disabling the subsystem.
+ *
+ * \since This function is available since SDL 2.0.22.
+ *
+ * \sa SDL_StartTextInput
+ * \sa SDL_StopTextInput
+ */
+extern DECLSPEC void SDLCALL SDL_ClearComposition(void);
+
+/**
+ * Returns if an IME Composite or Candidate window is currently shown.
+ *
+ * \since This function is available since SDL 2.0.22.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_IsTextInputShown(void);
+
+/**
+ * Set the rectangle used to type Unicode text inputs.
+ *
+ * Native input methods will place a window with word suggestions near it,
+ * without covering the text being inputted.
+ *
+ * To start text input in a given location, this function is intended to be
+ * called before SDL_StartTextInput, although some platforms support moving
+ * the rectangle even while text input (and a composition) is active.
+ *
+ * Note: If you want to use the system native IME window, try setting hint
+ * **SDL_HINT_IME_SHOW_UI** to **1**, otherwise this function won't give you
+ * any feedback.
+ *
+ * \param rect the SDL_Rect structure representing the rectangle to receive
+ * text (ignored if NULL).
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_StartTextInput
+ */
+extern DECLSPEC void SDLCALL SDL_SetTextInputRect(const SDL_Rect *rect);
+
+/**
+ * Check whether the platform has screen keyboard support.
+ *
+ * \returns SDL_TRUE if the platform has some screen keyboard support or
+ * SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_StartTextInput
+ * \sa SDL_IsScreenKeyboardShown
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_HasScreenKeyboardSupport(void);
+
+/**
+ * Check whether the screen keyboard is shown for given window.
+ *
+ * \param window the window for which screen keyboard should be queried.
+ * \returns SDL_TRUE if screen keyboard is shown or SDL_FALSE if not.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_HasScreenKeyboardSupport
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_IsScreenKeyboardShown(SDL_Window *window);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_keyboard_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_keycode.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_keycode.h
new file mode 100644
index 00000000..eb1678e3
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_keycode.h
@@ -0,0 +1,358 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryKeycode
+ *
+ * Defines constants which identify keyboard keys and modifiers.
+ */
+
+#ifndef SDL_keycode_h_
+#define SDL_keycode_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_scancode.h"
+
+/**
+ * The SDL virtual key representation.
+ *
+ * Values of this type are used to represent keyboard keys using the current
+ * layout of the keyboard. These values include Unicode values representing
+ * the unmodified character that would be generated by pressing the key, or an
+ * SDLK_* constant for those keys that do not generate characters.
+ *
+ * A special exception is the number keys at the top of the keyboard which map
+ * to SDLK_0...SDLK_9 on AZERTY layouts.
+ */
+typedef Sint32 SDL_Keycode;
+
+#define SDLK_SCANCODE_MASK (1<<30)
+#define SDL_SCANCODE_TO_KEYCODE(X) (X | SDLK_SCANCODE_MASK)
+
+typedef enum SDL_KeyCode
+{
+ SDLK_UNKNOWN = 0,
+
+ SDLK_RETURN = '\r',
+ SDLK_ESCAPE = '\x1B',
+ SDLK_BACKSPACE = '\b',
+ SDLK_TAB = '\t',
+ SDLK_SPACE = ' ',
+ SDLK_EXCLAIM = '!',
+ SDLK_QUOTEDBL = '"',
+ SDLK_HASH = '#',
+ SDLK_PERCENT = '%',
+ SDLK_DOLLAR = '$',
+ SDLK_AMPERSAND = '&',
+ SDLK_QUOTE = '\'',
+ SDLK_LEFTPAREN = '(',
+ SDLK_RIGHTPAREN = ')',
+ SDLK_ASTERISK = '*',
+ SDLK_PLUS = '+',
+ SDLK_COMMA = ',',
+ SDLK_MINUS = '-',
+ SDLK_PERIOD = '.',
+ SDLK_SLASH = '/',
+ SDLK_0 = '0',
+ SDLK_1 = '1',
+ SDLK_2 = '2',
+ SDLK_3 = '3',
+ SDLK_4 = '4',
+ SDLK_5 = '5',
+ SDLK_6 = '6',
+ SDLK_7 = '7',
+ SDLK_8 = '8',
+ SDLK_9 = '9',
+ SDLK_COLON = ':',
+ SDLK_SEMICOLON = ';',
+ SDLK_LESS = '<',
+ SDLK_EQUALS = '=',
+ SDLK_GREATER = '>',
+ SDLK_QUESTION = '?',
+ SDLK_AT = '@',
+
+ /*
+ Skip uppercase letters
+ */
+
+ SDLK_LEFTBRACKET = '[',
+ SDLK_BACKSLASH = '\\',
+ SDLK_RIGHTBRACKET = ']',
+ SDLK_CARET = '^',
+ SDLK_UNDERSCORE = '_',
+ SDLK_BACKQUOTE = '`',
+ SDLK_a = 'a',
+ SDLK_b = 'b',
+ SDLK_c = 'c',
+ SDLK_d = 'd',
+ SDLK_e = 'e',
+ SDLK_f = 'f',
+ SDLK_g = 'g',
+ SDLK_h = 'h',
+ SDLK_i = 'i',
+ SDLK_j = 'j',
+ SDLK_k = 'k',
+ SDLK_l = 'l',
+ SDLK_m = 'm',
+ SDLK_n = 'n',
+ SDLK_o = 'o',
+ SDLK_p = 'p',
+ SDLK_q = 'q',
+ SDLK_r = 'r',
+ SDLK_s = 's',
+ SDLK_t = 't',
+ SDLK_u = 'u',
+ SDLK_v = 'v',
+ SDLK_w = 'w',
+ SDLK_x = 'x',
+ SDLK_y = 'y',
+ SDLK_z = 'z',
+
+ SDLK_CAPSLOCK = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CAPSLOCK),
+
+ SDLK_F1 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F1),
+ SDLK_F2 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F2),
+ SDLK_F3 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F3),
+ SDLK_F4 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F4),
+ SDLK_F5 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F5),
+ SDLK_F6 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F6),
+ SDLK_F7 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F7),
+ SDLK_F8 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F8),
+ SDLK_F9 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F9),
+ SDLK_F10 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F10),
+ SDLK_F11 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F11),
+ SDLK_F12 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F12),
+
+ SDLK_PRINTSCREEN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PRINTSCREEN),
+ SDLK_SCROLLLOCK = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SCROLLLOCK),
+ SDLK_PAUSE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PAUSE),
+ SDLK_INSERT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_INSERT),
+ SDLK_HOME = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_HOME),
+ SDLK_PAGEUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PAGEUP),
+ SDLK_DELETE = '\x7F',
+ SDLK_END = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_END),
+ SDLK_PAGEDOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PAGEDOWN),
+ SDLK_RIGHT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RIGHT),
+ SDLK_LEFT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LEFT),
+ SDLK_DOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_DOWN),
+ SDLK_UP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_UP),
+
+ SDLK_NUMLOCKCLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_NUMLOCKCLEAR),
+ SDLK_KP_DIVIDE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DIVIDE),
+ SDLK_KP_MULTIPLY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MULTIPLY),
+ SDLK_KP_MINUS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MINUS),
+ SDLK_KP_PLUS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PLUS),
+ SDLK_KP_ENTER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_ENTER),
+ SDLK_KP_1 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_1),
+ SDLK_KP_2 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_2),
+ SDLK_KP_3 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_3),
+ SDLK_KP_4 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_4),
+ SDLK_KP_5 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_5),
+ SDLK_KP_6 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_6),
+ SDLK_KP_7 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_7),
+ SDLK_KP_8 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_8),
+ SDLK_KP_9 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_9),
+ SDLK_KP_0 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_0),
+ SDLK_KP_PERIOD = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PERIOD),
+
+ SDLK_APPLICATION = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_APPLICATION),
+ SDLK_POWER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_POWER),
+ SDLK_KP_EQUALS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_EQUALS),
+ SDLK_F13 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F13),
+ SDLK_F14 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F14),
+ SDLK_F15 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F15),
+ SDLK_F16 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F16),
+ SDLK_F17 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F17),
+ SDLK_F18 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F18),
+ SDLK_F19 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F19),
+ SDLK_F20 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F20),
+ SDLK_F21 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F21),
+ SDLK_F22 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F22),
+ SDLK_F23 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F23),
+ SDLK_F24 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_F24),
+ SDLK_EXECUTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_EXECUTE),
+ SDLK_HELP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_HELP),
+ SDLK_MENU = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MENU),
+ SDLK_SELECT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SELECT),
+ SDLK_STOP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_STOP),
+ SDLK_AGAIN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AGAIN),
+ SDLK_UNDO = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_UNDO),
+ SDLK_CUT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CUT),
+ SDLK_COPY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_COPY),
+ SDLK_PASTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PASTE),
+ SDLK_FIND = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_FIND),
+ SDLK_MUTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MUTE),
+ SDLK_VOLUMEUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_VOLUMEUP),
+ SDLK_VOLUMEDOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_VOLUMEDOWN),
+ SDLK_KP_COMMA = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_COMMA),
+ SDLK_KP_EQUALSAS400 =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_EQUALSAS400),
+
+ SDLK_ALTERASE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_ALTERASE),
+ SDLK_SYSREQ = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SYSREQ),
+ SDLK_CANCEL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CANCEL),
+ SDLK_CLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CLEAR),
+ SDLK_PRIOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_PRIOR),
+ SDLK_RETURN2 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RETURN2),
+ SDLK_SEPARATOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SEPARATOR),
+ SDLK_OUT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_OUT),
+ SDLK_OPER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_OPER),
+ SDLK_CLEARAGAIN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CLEARAGAIN),
+ SDLK_CRSEL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CRSEL),
+ SDLK_EXSEL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_EXSEL),
+
+ SDLK_KP_00 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_00),
+ SDLK_KP_000 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_000),
+ SDLK_THOUSANDSSEPARATOR =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_THOUSANDSSEPARATOR),
+ SDLK_DECIMALSEPARATOR =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_DECIMALSEPARATOR),
+ SDLK_CURRENCYUNIT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CURRENCYUNIT),
+ SDLK_CURRENCYSUBUNIT =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CURRENCYSUBUNIT),
+ SDLK_KP_LEFTPAREN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_LEFTPAREN),
+ SDLK_KP_RIGHTPAREN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_RIGHTPAREN),
+ SDLK_KP_LEFTBRACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_LEFTBRACE),
+ SDLK_KP_RIGHTBRACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_RIGHTBRACE),
+ SDLK_KP_TAB = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_TAB),
+ SDLK_KP_BACKSPACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_BACKSPACE),
+ SDLK_KP_A = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_A),
+ SDLK_KP_B = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_B),
+ SDLK_KP_C = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_C),
+ SDLK_KP_D = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_D),
+ SDLK_KP_E = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_E),
+ SDLK_KP_F = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_F),
+ SDLK_KP_XOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_XOR),
+ SDLK_KP_POWER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_POWER),
+ SDLK_KP_PERCENT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PERCENT),
+ SDLK_KP_LESS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_LESS),
+ SDLK_KP_GREATER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_GREATER),
+ SDLK_KP_AMPERSAND = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_AMPERSAND),
+ SDLK_KP_DBLAMPERSAND =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DBLAMPERSAND),
+ SDLK_KP_VERTICALBAR =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_VERTICALBAR),
+ SDLK_KP_DBLVERTICALBAR =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DBLVERTICALBAR),
+ SDLK_KP_COLON = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_COLON),
+ SDLK_KP_HASH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_HASH),
+ SDLK_KP_SPACE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_SPACE),
+ SDLK_KP_AT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_AT),
+ SDLK_KP_EXCLAM = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_EXCLAM),
+ SDLK_KP_MEMSTORE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMSTORE),
+ SDLK_KP_MEMRECALL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMRECALL),
+ SDLK_KP_MEMCLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMCLEAR),
+ SDLK_KP_MEMADD = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMADD),
+ SDLK_KP_MEMSUBTRACT =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMSUBTRACT),
+ SDLK_KP_MEMMULTIPLY =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMMULTIPLY),
+ SDLK_KP_MEMDIVIDE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_MEMDIVIDE),
+ SDLK_KP_PLUSMINUS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_PLUSMINUS),
+ SDLK_KP_CLEAR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_CLEAR),
+ SDLK_KP_CLEARENTRY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_CLEARENTRY),
+ SDLK_KP_BINARY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_BINARY),
+ SDLK_KP_OCTAL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_OCTAL),
+ SDLK_KP_DECIMAL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_DECIMAL),
+ SDLK_KP_HEXADECIMAL =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KP_HEXADECIMAL),
+
+ SDLK_LCTRL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LCTRL),
+ SDLK_LSHIFT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LSHIFT),
+ SDLK_LALT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LALT),
+ SDLK_LGUI = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_LGUI),
+ SDLK_RCTRL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RCTRL),
+ SDLK_RSHIFT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RSHIFT),
+ SDLK_RALT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RALT),
+ SDLK_RGUI = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_RGUI),
+
+ SDLK_MODE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MODE),
+
+ SDLK_AUDIONEXT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIONEXT),
+ SDLK_AUDIOPREV = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOPREV),
+ SDLK_AUDIOSTOP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOSTOP),
+ SDLK_AUDIOPLAY = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOPLAY),
+ SDLK_AUDIOMUTE = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOMUTE),
+ SDLK_MEDIASELECT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MEDIASELECT),
+ SDLK_WWW = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_WWW),
+ SDLK_MAIL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_MAIL),
+ SDLK_CALCULATOR = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CALCULATOR),
+ SDLK_COMPUTER = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_COMPUTER),
+ SDLK_AC_SEARCH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_SEARCH),
+ SDLK_AC_HOME = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_HOME),
+ SDLK_AC_BACK = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_BACK),
+ SDLK_AC_FORWARD = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_FORWARD),
+ SDLK_AC_STOP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_STOP),
+ SDLK_AC_REFRESH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_REFRESH),
+ SDLK_AC_BOOKMARKS = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AC_BOOKMARKS),
+
+ SDLK_BRIGHTNESSDOWN =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_BRIGHTNESSDOWN),
+ SDLK_BRIGHTNESSUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_BRIGHTNESSUP),
+ SDLK_DISPLAYSWITCH = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_DISPLAYSWITCH),
+ SDLK_KBDILLUMTOGGLE =
+ SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KBDILLUMTOGGLE),
+ SDLK_KBDILLUMDOWN = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KBDILLUMDOWN),
+ SDLK_KBDILLUMUP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_KBDILLUMUP),
+ SDLK_EJECT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_EJECT),
+ SDLK_SLEEP = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SLEEP),
+ SDLK_APP1 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_APP1),
+ SDLK_APP2 = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_APP2),
+
+ SDLK_AUDIOREWIND = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOREWIND),
+ SDLK_AUDIOFASTFORWARD = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_AUDIOFASTFORWARD),
+
+ SDLK_SOFTLEFT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SOFTLEFT),
+ SDLK_SOFTRIGHT = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SOFTRIGHT),
+ SDLK_CALL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CALL),
+ SDLK_ENDCALL = SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_ENDCALL)
+} SDL_KeyCode;
+
+/**
+ * Enumeration of valid key mods (possibly OR'd together).
+ */
+typedef enum SDL_Keymod
+{
+ KMOD_NONE = 0x0000,
+ KMOD_LSHIFT = 0x0001,
+ KMOD_RSHIFT = 0x0002,
+ KMOD_LCTRL = 0x0040,
+ KMOD_RCTRL = 0x0080,
+ KMOD_LALT = 0x0100,
+ KMOD_RALT = 0x0200,
+ KMOD_LGUI = 0x0400,
+ KMOD_RGUI = 0x0800,
+ KMOD_NUM = 0x1000,
+ KMOD_CAPS = 0x2000,
+ KMOD_MODE = 0x4000,
+ KMOD_SCROLL = 0x8000,
+
+ KMOD_CTRL = KMOD_LCTRL | KMOD_RCTRL,
+ KMOD_SHIFT = KMOD_LSHIFT | KMOD_RSHIFT,
+ KMOD_ALT = KMOD_LALT | KMOD_RALT,
+ KMOD_GUI = KMOD_LGUI | KMOD_RGUI,
+
+ KMOD_RESERVED = KMOD_SCROLL /* This is for source-level compatibility with SDL 2.0.0. */
+} SDL_Keymod;
+
+#endif /* SDL_keycode_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_loadso.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_loadso.h
new file mode 100644
index 00000000..1763b528
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_loadso.h
@@ -0,0 +1,117 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/* WIKI CATEGORY: LoadSO */
+
+/**
+ * # CategoryLoadSO
+ *
+ * System-dependent library loading routines.
+ *
+ * Some things to keep in mind:
+ *
+ * - These functions only work on C function names. Other languages may have
+ * name mangling and intrinsic language support that varies from compiler to
+ * compiler.
+ * - Make sure you declare your function pointers with the same calling
+ * convention as the actual library function. Your code will crash
+ * mysteriously if you do not do this.
+ * - Avoid namespace collisions. If you load a symbol from the library, it is
+ * not defined whether or not it goes into the global symbol namespace for
+ * the application. If it does and it conflicts with symbols in your code or
+ * other shared libraries, you will not get the results you expect. :)
+ */
+
+#ifndef SDL_loadso_h_
+#define SDL_loadso_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Dynamically load a shared object.
+ *
+ * \param sofile a system-dependent name of the object file.
+ * \returns an opaque pointer to the object handle or NULL if there was an
+ * error; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LoadFunction
+ * \sa SDL_UnloadObject
+ */
+extern DECLSPEC void *SDLCALL SDL_LoadObject(const char *sofile);
+
+/**
+ * Look up the address of the named function in a shared object.
+ *
+ * This function pointer is no longer valid after calling SDL_UnloadObject().
+ *
+ * This function can only look up C function names. Other languages may have
+ * name mangling and intrinsic language support that varies from compiler to
+ * compiler.
+ *
+ * Make sure you declare your function pointers with the same calling
+ * convention as the actual library function. Your code will crash
+ * mysteriously if you do not do this.
+ *
+ * If the requested function doesn't exist, NULL is returned.
+ *
+ * \param handle a valid shared object handle returned by SDL_LoadObject().
+ * \param name the name of the function to look up.
+ * \returns a pointer to the function or NULL if there was an error; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LoadObject
+ * \sa SDL_UnloadObject
+ */
+extern DECLSPEC void *SDLCALL SDL_LoadFunction(void *handle,
+ const char *name);
+
+/**
+ * Unload a shared object from memory.
+ *
+ * \param handle a valid shared object handle returned by SDL_LoadObject().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LoadFunction
+ * \sa SDL_LoadObject
+ */
+extern DECLSPEC void SDLCALL SDL_UnloadObject(void *handle);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_loadso_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_locale.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_locale.h
new file mode 100644
index 00000000..8126efc7
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_locale.h
@@ -0,0 +1,103 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryLocale
+ *
+ * Include file for SDL locale services
+ */
+
+#ifndef _SDL_locale_h
+#define _SDL_locale_h
+
+#include "SDL_stdinc.h"
+#include "SDL_error.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+/* *INDENT-OFF* */
+extern "C" {
+/* *INDENT-ON* */
+#endif
+
+
+typedef struct SDL_Locale
+{
+ const char *language; /**< A language name, like "en" for English. */
+ const char *country; /**< A country, like "US" for America. Can be NULL. */
+} SDL_Locale;
+
+/**
+ * Report the user's preferred locale.
+ *
+ * This returns an array of SDL_Locale structs, the final item zeroed out.
+ * When the caller is done with this array, it should call SDL_free() on the
+ * returned value; all the memory involved is allocated in a single block, so
+ * a single SDL_free() will suffice.
+ *
+ * Returned language strings are in the format xx, where 'xx' is an ISO-639
+ * language specifier (such as "en" for English, "de" for German, etc).
+ * Country strings are in the format YY, where "YY" is an ISO-3166 country
+ * code (such as "US" for the United States, "CA" for Canada, etc). Country
+ * might be NULL if there's no specific guidance on them (so you might get {
+ * "en", "US" } for American English, but { "en", NULL } means "English
+ * language, generically"). Language strings are never NULL, except to
+ * terminate the array.
+ *
+ * Please note that not all of these strings are 2 characters; some are three
+ * or more.
+ *
+ * The returned list of locales are in the order of the user's preference. For
+ * example, a German citizen that is fluent in US English and knows enough
+ * Japanese to navigate around Tokyo might have a list like: { "de", "en_US",
+ * "jp", NULL }. Someone from England might prefer British English (where
+ * "color" is spelled "colour", etc), but will settle for anything like it: {
+ * "en_GB", "en", NULL }.
+ *
+ * This function returns NULL on error, including when the platform does not
+ * supply this information at all.
+ *
+ * This might be a "slow" call that has to query the operating system. It's
+ * best to ask for this once and save the results. However, this list can
+ * change, usually because the user has changed a system preference outside of
+ * your program; SDL will send an SDL_LOCALECHANGED event in this case, if
+ * possible, and you can call this function again to get an updated copy of
+ * preferred locales.
+ *
+ * \return array of locales, terminated with a locale with a NULL language
+ * field. Will return NULL on error.
+ *
+ * \since This function is available since SDL 2.0.14.
+ */
+extern DECLSPEC SDL_Locale * SDLCALL SDL_GetPreferredLocales(void);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+/* *INDENT-OFF* */
+}
+/* *INDENT-ON* */
+#endif
+#include "close_code.h"
+
+#endif /* _SDL_locale_h */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_log.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_log.h
new file mode 100644
index 00000000..75833ba3
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_log.h
@@ -0,0 +1,405 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ * # CategoryLog
+ *
+ * Simple log messages with categories and priorities.
+ *
+ * By default logs are quiet, but if you're debugging SDL you might want:
+ *
+ * SDL_LogSetAllPriority(SDL_LOG_PRIORITY_WARN);
+ *
+ * Here's where the messages go on different platforms:
+ *
+ * - Windows: debug output stream
+ * - Android: log output
+ * - Others: standard error output (stderr)
+ */
+
+#ifndef SDL_log_h_
+#define SDL_log_h_
+
+#include "SDL_stdinc.h"
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/**
+ * The maximum size of a log message prior to SDL 2.0.24
+ *
+ * As of 2.0.24 there is no limit to the length of SDL log messages.
+ */
+#define SDL_MAX_LOG_MESSAGE 4096
+
+/**
+ * The predefined log categories
+ *
+ * By default the application category is enabled at the INFO level, the
+ * assert category is enabled at the WARN level, test is enabled at the
+ * VERBOSE level and all other categories are enabled at the ERROR level.
+ */
+typedef enum SDL_LogCategory
+{
+ SDL_LOG_CATEGORY_APPLICATION,
+ SDL_LOG_CATEGORY_ERROR,
+ SDL_LOG_CATEGORY_ASSERT,
+ SDL_LOG_CATEGORY_SYSTEM,
+ SDL_LOG_CATEGORY_AUDIO,
+ SDL_LOG_CATEGORY_VIDEO,
+ SDL_LOG_CATEGORY_RENDER,
+ SDL_LOG_CATEGORY_INPUT,
+ SDL_LOG_CATEGORY_TEST,
+
+ /* Reserved for future SDL library use */
+ SDL_LOG_CATEGORY_RESERVED1,
+ SDL_LOG_CATEGORY_RESERVED2,
+ SDL_LOG_CATEGORY_RESERVED3,
+ SDL_LOG_CATEGORY_RESERVED4,
+ SDL_LOG_CATEGORY_RESERVED5,
+ SDL_LOG_CATEGORY_RESERVED6,
+ SDL_LOG_CATEGORY_RESERVED7,
+ SDL_LOG_CATEGORY_RESERVED8,
+ SDL_LOG_CATEGORY_RESERVED9,
+ SDL_LOG_CATEGORY_RESERVED10,
+
+ /* Beyond this point is reserved for application use, e.g.
+ enum {
+ MYAPP_CATEGORY_AWESOME1 = SDL_LOG_CATEGORY_CUSTOM,
+ MYAPP_CATEGORY_AWESOME2,
+ MYAPP_CATEGORY_AWESOME3,
+ ...
+ };
+ */
+ SDL_LOG_CATEGORY_CUSTOM
+} SDL_LogCategory;
+
+/**
+ * The predefined log priorities
+ */
+typedef enum SDL_LogPriority
+{
+ SDL_LOG_PRIORITY_VERBOSE = 1,
+ SDL_LOG_PRIORITY_DEBUG,
+ SDL_LOG_PRIORITY_INFO,
+ SDL_LOG_PRIORITY_WARN,
+ SDL_LOG_PRIORITY_ERROR,
+ SDL_LOG_PRIORITY_CRITICAL,
+ SDL_NUM_LOG_PRIORITIES
+} SDL_LogPriority;
+
+
+/**
+ * Set the priority of all log categories.
+ *
+ * \param priority the SDL_LogPriority to assign.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LogSetPriority
+ */
+extern DECLSPEC void SDLCALL SDL_LogSetAllPriority(SDL_LogPriority priority);
+
+/**
+ * Set the priority of a particular log category.
+ *
+ * \param category the category to assign a priority to.
+ * \param priority the SDL_LogPriority to assign.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LogGetPriority
+ * \sa SDL_LogSetAllPriority
+ */
+extern DECLSPEC void SDLCALL SDL_LogSetPriority(int category,
+ SDL_LogPriority priority);
+
+/**
+ * Get the priority of a particular log category.
+ *
+ * \param category the category to query.
+ * \returns the SDL_LogPriority for the requested category.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LogSetPriority
+ */
+extern DECLSPEC SDL_LogPriority SDLCALL SDL_LogGetPriority(int category);
+
+/**
+ * Reset all priorities to default.
+ *
+ * This is called by SDL_Quit().
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LogSetAllPriority
+ * \sa SDL_LogSetPriority
+ */
+extern DECLSPEC void SDLCALL SDL_LogResetPriorities(void);
+
+/**
+ * Log a message with SDL_LOG_CATEGORY_APPLICATION and SDL_LOG_PRIORITY_INFO.
+ *
+ * = * \param fmt a printf() style message format string
+ *
+ * \param ... additional parameters matching % tokens in the `fmt` string, if
+ * any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LogCritical
+ * \sa SDL_LogDebug
+ * \sa SDL_LogError
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessage
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogVerbose
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_Log(SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(1);
+
+/**
+ * Log a message with SDL_LOG_PRIORITY_VERBOSE.
+ *
+ * \param category the category of the message.
+ * \param fmt a printf() style message format string.
+ * \param ... additional parameters matching % tokens in the **fmt** string,
+ * if any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogCritical
+ * \sa SDL_LogDebug
+ * \sa SDL_LogError
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessage
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_LogVerbose(int category, SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(2);
+
+/**
+ * Log a message with SDL_LOG_PRIORITY_DEBUG.
+ *
+ * \param category the category of the message.
+ * \param fmt a printf() style message format string.
+ * \param ... additional parameters matching % tokens in the **fmt** string,
+ * if any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogCritical
+ * \sa SDL_LogError
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessage
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogVerbose
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_LogDebug(int category, SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(2);
+
+/**
+ * Log a message with SDL_LOG_PRIORITY_INFO.
+ *
+ * \param category the category of the message.
+ * \param fmt a printf() style message format string.
+ * \param ... additional parameters matching % tokens in the **fmt** string,
+ * if any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogCritical
+ * \sa SDL_LogDebug
+ * \sa SDL_LogError
+ * \sa SDL_LogMessage
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogVerbose
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_LogInfo(int category, SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(2);
+
+/**
+ * Log a message with SDL_LOG_PRIORITY_WARN.
+ *
+ * \param category the category of the message.
+ * \param fmt a printf() style message format string.
+ * \param ... additional parameters matching % tokens in the **fmt** string,
+ * if any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogCritical
+ * \sa SDL_LogDebug
+ * \sa SDL_LogError
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessage
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogVerbose
+ */
+extern DECLSPEC void SDLCALL SDL_LogWarn(int category, SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(2);
+
+/**
+ * Log a message with SDL_LOG_PRIORITY_ERROR.
+ *
+ * \param category the category of the message.
+ * \param fmt a printf() style message format string.
+ * \param ... additional parameters matching % tokens in the **fmt** string,
+ * if any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogCritical
+ * \sa SDL_LogDebug
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessage
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogVerbose
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_LogError(int category, SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(2);
+
+/**
+ * Log a message with SDL_LOG_PRIORITY_CRITICAL.
+ *
+ * \param category the category of the message.
+ * \param fmt a printf() style message format string.
+ * \param ... additional parameters matching % tokens in the **fmt** string,
+ * if any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogDebug
+ * \sa SDL_LogError
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessage
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogVerbose
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_LogCritical(int category, SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(2);
+
+/**
+ * Log a message with the specified category and priority.
+ *
+ * \param category the category of the message.
+ * \param priority the priority of the message.
+ * \param fmt a printf() style message format string.
+ * \param ... additional parameters matching % tokens in the **fmt** string,
+ * if any.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogCritical
+ * \sa SDL_LogDebug
+ * \sa SDL_LogError
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessageV
+ * \sa SDL_LogVerbose
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_LogMessage(int category,
+ SDL_LogPriority priority,
+ SDL_PRINTF_FORMAT_STRING const char *fmt, ...) SDL_PRINTF_VARARG_FUNC(3);
+
+/**
+ * Log a message with the specified category and priority.
+ *
+ * \param category the category of the message.
+ * \param priority the priority of the message.
+ * \param fmt a printf() style message format string.
+ * \param ap a variable argument list.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Log
+ * \sa SDL_LogCritical
+ * \sa SDL_LogDebug
+ * \sa SDL_LogError
+ * \sa SDL_LogInfo
+ * \sa SDL_LogMessage
+ * \sa SDL_LogVerbose
+ * \sa SDL_LogWarn
+ */
+extern DECLSPEC void SDLCALL SDL_LogMessageV(int category,
+ SDL_LogPriority priority,
+ SDL_PRINTF_FORMAT_STRING const char *fmt, va_list ap) SDL_PRINTF_VARARG_FUNCV(3);
+
+/**
+ * The prototype for the log output callback function.
+ *
+ * This function is called by SDL when there is new text to be logged.
+ *
+ * \param userdata what was passed as `userdata` to
+ * SDL_LogSetOutputFunction().
+ * \param category the category of the message.
+ * \param priority the priority of the message.
+ * \param message the message being output.
+ */
+typedef void (SDLCALL *SDL_LogOutputFunction)(void *userdata, int category, SDL_LogPriority priority, const char *message);
+
+/**
+ * Get the current log output function.
+ *
+ * \param callback an SDL_LogOutputFunction filled in with the current log
+ * callback.
+ * \param userdata a pointer filled in with the pointer that is passed to
+ * `callback`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LogSetOutputFunction
+ */
+extern DECLSPEC void SDLCALL SDL_LogGetOutputFunction(SDL_LogOutputFunction *callback, void **userdata);
+
+/**
+ * Replace the default log output function with one of your own.
+ *
+ * \param callback an SDL_LogOutputFunction to call instead of the default.
+ * \param userdata a pointer that is passed to `callback`.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_LogGetOutputFunction
+ */
+extern DECLSPEC void SDLCALL SDL_LogSetOutputFunction(SDL_LogOutputFunction callback, void *userdata);
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_log_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_main.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_main.h
new file mode 100644
index 00000000..a1ef3e74
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_main.h
@@ -0,0 +1,282 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_main_h_
+#define SDL_main_h_
+
+#include "SDL_stdinc.h"
+
+/**
+ * # CategoryMain
+ *
+ * Redefine main() on some platforms so that it is called by SDL.
+ */
+
+#ifndef SDL_MAIN_HANDLED
+#if defined(__WIN32__)
+/* On Windows SDL provides WinMain(), which parses the command line and passes
+ the arguments to your main function.
+
+ If you provide your own WinMain(), you may define SDL_MAIN_HANDLED
+ */
+#define SDL_MAIN_AVAILABLE
+
+#elif defined(__WINRT__)
+/* On WinRT, SDL provides a main function that initializes CoreApplication,
+ creating an instance of IFrameworkView in the process.
+
+ Please note that #include'ing SDL_main.h is not enough to get a main()
+ function working. In non-XAML apps, the file,
+ src/main/winrt/SDL_WinRT_main_NonXAML.cpp, or a copy of it, must be compiled
+ into the app itself. In XAML apps, the function, SDL_WinRTRunApp must be
+ called, with a pointer to the Direct3D-hosted XAML control passed in.
+*/
+#define SDL_MAIN_NEEDED
+
+#elif defined(__GDK__)
+/* On GDK, SDL provides a main function that initializes the game runtime.
+
+ Please note that #include'ing SDL_main.h is not enough to get a main()
+ function working. You must either link against SDL2main or, if not possible,
+ call the SDL_GDKRunApp function from your entry point.
+*/
+#define SDL_MAIN_NEEDED
+
+#elif defined(__IPHONEOS__)
+/* On iOS SDL provides a main function that creates an application delegate
+ and starts the iOS application run loop.
+
+ If you link with SDL dynamically on iOS, the main function can't be in a
+ shared library, so you need to link with libSDLmain.a, which includes a
+ stub main function that calls into the shared library to start execution.
+
+ See src/video/uikit/SDL_uikitappdelegate.m for more details.
+ */
+#define SDL_MAIN_NEEDED
+
+#elif defined(__ANDROID__)
+/* On Android SDL provides a Java class in SDLActivity.java that is the
+ main activity entry point.
+
+ See docs/README-android.md for more details on extending that class.
+ */
+#define SDL_MAIN_NEEDED
+
+/* We need to export SDL_main so it can be launched from Java */
+#define SDLMAIN_DECLSPEC DECLSPEC
+
+#elif defined(__NACL__)
+/* On NACL we use ppapi_simple to set up the application helper code,
+ then wait for the first PSE_INSTANCE_DIDCHANGEVIEW event before
+ starting the user main function.
+ All user code is run in a separate thread by ppapi_simple, thus
+ allowing for blocking io to take place via nacl_io
+*/
+#define SDL_MAIN_NEEDED
+
+#elif defined(__PSP__)
+/* On PSP SDL provides a main function that sets the module info,
+ activates the GPU and starts the thread required to be able to exit
+ the software.
+
+ If you provide this yourself, you may define SDL_MAIN_HANDLED
+ */
+#define SDL_MAIN_AVAILABLE
+
+#elif defined(__PS2__)
+#define SDL_MAIN_AVAILABLE
+
+#define SDL_PS2_SKIP_IOP_RESET() \
+ void reset_IOP(); \
+ void reset_IOP() {}
+
+#elif defined(__3DS__)
+/*
+ On N3DS, SDL provides a main function that sets up the screens
+ and storage.
+
+ If you provide this yourself, you may define SDL_MAIN_HANDLED
+*/
+#define SDL_MAIN_AVAILABLE
+
+#endif
+#endif /* SDL_MAIN_HANDLED */
+
+#ifndef SDLMAIN_DECLSPEC
+#define SDLMAIN_DECLSPEC
+#endif
+
+/**
+ * \file SDL_main.h
+ *
+ * The application's main() function must be called with C linkage,
+ * and should be declared like this:
+ * ```c
+ * #ifdef __cplusplus
+ * extern "C"
+ * #endif
+ * int main(int argc, char *argv[])
+ * {
+ * }
+ * ```
+ */
+
+#if defined(SDL_MAIN_NEEDED) || defined(SDL_MAIN_AVAILABLE)
+#define main SDL_main
+#endif
+
+#include "begin_code.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The prototype for the application's main() function
+ */
+typedef int (*SDL_main_func)(int argc, char *argv[]);
+extern SDLMAIN_DECLSPEC int SDL_main(int argc, char *argv[]);
+
+
+/**
+ * Circumvent failure of SDL_Init() when not using SDL_main() as an entry
+ * point.
+ *
+ * This function is defined in SDL_main.h, along with the preprocessor rule to
+ * redefine main() as SDL_main(). Thus to ensure that your main() function
+ * will not be changed it is necessary to define SDL_MAIN_HANDLED before
+ * including SDL.h.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_Init
+ */
+extern DECLSPEC void SDLCALL SDL_SetMainReady(void);
+
+#if defined(__WIN32__) || defined(__GDK__)
+
+/**
+ * Register a win32 window class for SDL's use.
+ *
+ * This can be called to set the application window class at startup. It is
+ * safe to call this multiple times, as long as every call is eventually
+ * paired with a call to SDL_UnregisterApp, but a second registration attempt
+ * while a previous registration is still active will be ignored, other than
+ * to increment a counter.
+ *
+ * Most applications do not need to, and should not, call this directly; SDL
+ * will call it when initializing the video subsystem.
+ *
+ * \param name the window class name, in UTF-8 encoding. If NULL, SDL
+ * currently uses "SDL_app" but this isn't guaranteed.
+ * \param style the value to use in WNDCLASSEX::style. If `name` is NULL, SDL
+ * currently uses `(CS_BYTEALIGNCLIENT | CS_OWNDC)` regardless of
+ * what is specified here.
+ * \param hInst the HINSTANCE to use in WNDCLASSEX::hInstance. If zero, SDL
+ * will use `GetModuleHandle(NULL)` instead.
+ * \returns 0 on success, -1 on error. SDL_GetError() may have details.
+ *
+ * \since This function is available since SDL 2.0.2.
+ */
+extern DECLSPEC int SDLCALL SDL_RegisterApp(const char *name, Uint32 style, void *hInst);
+
+/**
+ * Deregister the win32 window class from an SDL_RegisterApp call.
+ *
+ * This can be called to undo the effects of SDL_RegisterApp.
+ *
+ * Most applications do not need to, and should not, call this directly; SDL
+ * will call it when deinitializing the video subsystem.
+ *
+ * It is safe to call this multiple times, as long as every call is eventually
+ * paired with a prior call to SDL_RegisterApp. The window class will only be
+ * deregistered when the registration counter in SDL_RegisterApp decrements to
+ * zero through calls to this function.
+ *
+ * \since This function is available since SDL 2.0.2.
+ */
+extern DECLSPEC void SDLCALL SDL_UnregisterApp(void);
+
+#endif /* defined(__WIN32__) || defined(__GDK__) */
+
+
+#ifdef __WINRT__
+
+/**
+ * Initialize and launch an SDL/WinRT application.
+ *
+ * \param mainFunction the SDL app's C-style main(), an SDL_main_func.
+ * \param reserved reserved for future use; should be NULL.
+ * \returns 0 on success or -1 on failure; call SDL_GetError() to retrieve
+ * more information on the failure.
+ *
+ * \since This function is available since SDL 2.0.3.
+ */
+extern DECLSPEC int SDLCALL SDL_WinRTRunApp(SDL_main_func mainFunction, void * reserved);
+
+#endif /* __WINRT__ */
+
+#if defined(__IPHONEOS__)
+
+/**
+ * Initializes and launches an SDL application.
+ *
+ * \param argc The argc parameter from the application's main() function.
+ * \param argv The argv parameter from the application's main() function.
+ * \param mainFunction The SDL app's C-style main(), an SDL_main_func.
+ * \return the return value from mainFunction.
+ *
+ * \since This function is available since SDL 2.0.10.
+ */
+extern DECLSPEC int SDLCALL SDL_UIKitRunApp(int argc, char *argv[], SDL_main_func mainFunction);
+
+#endif /* __IPHONEOS__ */
+
+#ifdef __GDK__
+
+/**
+ * Initialize and launch an SDL GDK application.
+ *
+ * \param mainFunction the SDL app's C-style main(), an SDL_main_func.
+ * \param reserved reserved for future use; should be NULL.
+ * \returns 0 on success or -1 on failure; call SDL_GetError() to retrieve
+ * more information on the failure.
+ *
+ * \since This function is available since SDL 2.24.0.
+ */
+extern DECLSPEC int SDLCALL SDL_GDKRunApp(SDL_main_func mainFunction, void *reserved);
+
+/**
+ * Callback from the application to let the suspend continue.
+ *
+ * \since This function is available since SDL 2.28.0.
+ */
+extern DECLSPEC void SDLCALL SDL_GDKSuspendComplete(void);
+
+#endif /* __GDK__ */
+
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_main_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_messagebox.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_messagebox.h
new file mode 100644
index 00000000..725d4124
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_messagebox.h
@@ -0,0 +1,196 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_messagebox_h_
+#define SDL_messagebox_h_
+
+#include "SDL_stdinc.h"
+#include "SDL_video.h" /* For SDL_Window */
+
+#include "begin_code.h"
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * SDL_MessageBox flags.
+ *
+ * If supported will display warning icon, etc.
+ */
+typedef enum SDL_MessageBoxFlags
+{
+ SDL_MESSAGEBOX_ERROR = 0x00000010, /**< error dialog */
+ SDL_MESSAGEBOX_WARNING = 0x00000020, /**< warning dialog */
+ SDL_MESSAGEBOX_INFORMATION = 0x00000040, /**< informational dialog */
+ SDL_MESSAGEBOX_BUTTONS_LEFT_TO_RIGHT = 0x00000080, /**< buttons placed left to right */
+ SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT = 0x00000100 /**< buttons placed right to left */
+} SDL_MessageBoxFlags;
+
+/**
+ * Flags for SDL_MessageBoxButtonData.
+ */
+typedef enum SDL_MessageBoxButtonFlags
+{
+ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT = 0x00000001, /**< Marks the default button when return is hit */
+ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT = 0x00000002 /**< Marks the default button when escape is hit */
+} SDL_MessageBoxButtonFlags;
+
+/**
+ * Individual button data.
+ */
+typedef struct SDL_MessageBoxButtonData
+{
+ Uint32 flags; /**< SDL_MessageBoxButtonFlags */
+ int buttonid; /**< User defined button id (value returned via SDL_ShowMessageBox) */
+ const char * text; /**< The UTF-8 button text */
+} SDL_MessageBoxButtonData;
+
+/**
+ * RGB value used in a message box color scheme
+ */
+typedef struct SDL_MessageBoxColor
+{
+ Uint8 r, g, b;
+} SDL_MessageBoxColor;
+
+typedef enum SDL_MessageBoxColorType
+{
+ SDL_MESSAGEBOX_COLOR_BACKGROUND,
+ SDL_MESSAGEBOX_COLOR_TEXT,
+ SDL_MESSAGEBOX_COLOR_BUTTON_BORDER,
+ SDL_MESSAGEBOX_COLOR_BUTTON_BACKGROUND,
+ SDL_MESSAGEBOX_COLOR_BUTTON_SELECTED,
+ SDL_MESSAGEBOX_COLOR_MAX
+} SDL_MessageBoxColorType;
+
+/**
+ * A set of colors to use for message box dialogs
+ */
+typedef struct SDL_MessageBoxColorScheme
+{
+ SDL_MessageBoxColor colors[SDL_MESSAGEBOX_COLOR_MAX];
+} SDL_MessageBoxColorScheme;
+
+/**
+ * MessageBox structure containing title, text, window, etc.
+ */
+typedef struct SDL_MessageBoxData
+{
+ Uint32 flags; /**< SDL_MessageBoxFlags */
+ SDL_Window *window; /**< Parent window, can be NULL */
+ const char *title; /**< UTF-8 title */
+ const char *message; /**< UTF-8 message text */
+
+ int numbuttons;
+ const SDL_MessageBoxButtonData *buttons;
+
+ const SDL_MessageBoxColorScheme *colorScheme; /**< SDL_MessageBoxColorScheme, can be NULL to use system settings */
+} SDL_MessageBoxData;
+
+/**
+ * Create a modal message box.
+ *
+ * If your needs aren't complex, it might be easier to use
+ * SDL_ShowSimpleMessageBox.
+ *
+ * This function should be called on the thread that created the parent
+ * window, or on the main thread if the messagebox has no parent. It will
+ * block execution of that thread until the user clicks a button or closes the
+ * messagebox.
+ *
+ * This function may be called at any time, even before SDL_Init(). This makes
+ * it useful for reporting errors like a failure to create a renderer or
+ * OpenGL context.
+ *
+ * On X11, SDL rolls its own dialog box with X11 primitives instead of a
+ * formal toolkit like GTK+ or Qt.
+ *
+ * Note that if SDL_Init() would fail because there isn't any available video
+ * target, this function is likely to fail for the same reasons. If this is a
+ * concern, check the return value from this function and fall back to writing
+ * to stderr if you can.
+ *
+ * \param messageboxdata the SDL_MessageBoxData structure with title, text and
+ * other options.
+ * \param buttonid the pointer to which user id of hit button should be
+ * copied.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_ShowSimpleMessageBox
+ */
+extern DECLSPEC int SDLCALL SDL_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonid);
+
+/**
+ * Display a simple modal message box.
+ *
+ * If your needs aren't complex, this function is preferred over
+ * SDL_ShowMessageBox.
+ *
+ * `flags` may be any of the following:
+ *
+ * - `SDL_MESSAGEBOX_ERROR`: error dialog
+ * - `SDL_MESSAGEBOX_WARNING`: warning dialog
+ * - `SDL_MESSAGEBOX_INFORMATION`: informational dialog
+ *
+ * This function should be called on the thread that created the parent
+ * window, or on the main thread if the messagebox has no parent. It will
+ * block execution of that thread until the user clicks a button or closes the
+ * messagebox.
+ *
+ * This function may be called at any time, even before SDL_Init(). This makes
+ * it useful for reporting errors like a failure to create a renderer or
+ * OpenGL context.
+ *
+ * On X11, SDL rolls its own dialog box with X11 primitives instead of a
+ * formal toolkit like GTK+ or Qt.
+ *
+ * Note that if SDL_Init() would fail because there isn't any available video
+ * target, this function is likely to fail for the same reasons. If this is a
+ * concern, check the return value from this function and fall back to writing
+ * to stderr if you can.
+ *
+ * \param flags an SDL_MessageBoxFlags value.
+ * \param title UTF-8 title text.
+ * \param message UTF-8 message text.
+ * \param window the parent window, or NULL for no parent.
+ * \returns 0 on success or a negative error code on failure; call
+ * SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 2.0.0.
+ *
+ * \sa SDL_ShowMessageBox
+ */
+extern DECLSPEC int SDLCALL SDL_ShowSimpleMessageBox(Uint32 flags, const char *title, const char *message, SDL_Window *window);
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include "close_code.h"
+
+#endif /* SDL_messagebox_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/ports/uwp/third_party/sdl2/include/SDL2/SDL_metal.h b/ports/uwp/third_party/sdl2/include/SDL2/SDL_metal.h
new file mode 100644
index 00000000..d3f21d5e
--- /dev/null
+++ b/ports/uwp/third_party/sdl2/include/SDL2/SDL_metal.h
@@ -0,0 +1,114 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2025 Sam Lantinga