[
  {
    "path": ".github/workflows/build.yml",
    "content": "# Automatically build the project and run any configured tests for every push\n# and submitted pull request. This can help catch issues that only occur on\n# certain platforms or Java versions, and provides a first line of defence\n# against bad commits.\n\nname: build\non: [pull_request, push]\n\njobs:\n  build:\n    strategy:\n      matrix:\n        # Use these Java versions\n        java: [\n            21    # Minimum supported by Minecraft\n        ]\n        # and run on both Linux and Windows\n        os: [ubuntu-22.04]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: checkout repository\n        uses: actions/checkout@v2\n      - name: validate gradle wrapper\n        uses: gradle/wrapper-validation-action@v1\n      - name: setup jdk ${{ matrix.java }}\n        uses: actions/setup-java@v1\n        with:\n          java-version: ${{ matrix.java }}\n      - name: make gradle wrapper executable\n        if: ${{ runner.os != 'Windows' }}\n        run: chmod +x ./gradlew\n      - name: build\n        run: ./gradlew build\n      - name: capture build artifacts\n        if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS\n        uses: actions/upload-artifact@v4\n        with:\n          name: Artifacts\n          path: build/libs/\n"
  },
  {
    "path": ".gitignore",
    "content": "# User-specific stuff\n.idea/\n\n*.iml\n*.ipr\n*.iws\n\n# IntelliJ\nout/\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# Windows thumbnail cache files\nThumbs.db\nThumbs.db:encryptable\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n.gradle\nbuild/\n\n# Ignore Gradle GUI config\ngradle-app.setting\n\n# Cache of project\n.gradletasknamecache\n\n**/build/\n\n# Common working directory\nrun/\n\n# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)\n!gradle-wrapper.jar\n\n# generated sources\n/src/*/generated/\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2021 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n    <img src=\"https://i.imgur.com/VXjFso4.png\">\n    <br>\n    oωo (owo-lib)\n    <br>\n    <a href=\"https://www.curseforge.com/minecraft/mc-mods/owo-lib\">\n        <img src=\"https://img.shields.io/badge/-CurseForge-gray?style=for-the-badge&logo=curseforge&labelColor=orange\">\n    </a>\n    <a href=\"https://modrinth.com/mod/owo-lib\">\n        <img src=\"https://img.shields.io/badge/-modrinth-gray?style=for-the-badge&labelColor=green&labelWidth=15&logo=appveyor&logoColor=white\">\n    </a>\n    <br>\n    <a href=\"https://github.com/wisp-forest/owo-lib/releases\">\n        <img src=\"https://img.shields.io/github/v/release/glisco03/owo-lib?logo=github&style=for-the-badge\">\n    </a>\n    <a href=\"https://discord.gg/xrwHKktV2d\">\n        <img src=\"https://img.shields.io/discord/825828008644313089?label=wisp%20forest&logo=discord&logoColor=white&style=for-the-badge\">\n    </a>\n</h1>\n    \n## Overview\n\nA general utility, GUI and config library for modding on Fabric. oωo is generally aimed at reducing code verbosity and making development more ergonomic. It covers a wide range of features from networking and serialization over GUI applications and configuration to data handling and registration. \n\n**Build Setup:**\n```properties\n# https://maven.wispforest.io/io/wispforest/owo-lib/\nowo_version=...\n```\n\n```groovy\nrepositories {\n    maven { url 'https://maven.wispforest.io' }\n}\n\n<...>\n\ndependencies {\n    modImplementation \"io.wispforest:owo-lib:${project.owo_version}\"\n    // only if you plan to use owo-config\n    annotationProcessor \"io.wispforest:owo-lib:${project.owo_version}\"\n    \n    // include this if you don't want force your users to install owo\n    // sentinel will warn them and give the option to download it automatically\n    include \"io.wispforest:owo-sentinel:${project.owo_version}\"\n}\n```\n\n<details>\n<summary><strong>Kotlin DSL</strong></summary>\n    \n```kotlin\nrepositories {\n    maven(\"https://maven.wispforest.io\")\n}\n    \ndependencies {\n    modImplementation(\"io.wispforest:owo-lib:${properties[\"owo_version\"]}\")\n    // only if you plan to use owo-config\n    annotationProcessor(\"io.wispforest:owo-lib:${properties[\"owo_version\"]}\")\n    \n    // include this if you don't want force your users to install owo\n    // sentinel will warn them and give the option to download it automatically\n    include(\"io.wispforest:owo-sentinel:${properties[\"owo_version\"]}\")\n} \n```\n    \n</details>\n\nYou can check the latest version on the [Releases](https://github.com/wisp-forest/owo-lib/releases) page\n\nowo is documented in two main ways:\n - There is rich, detailed JavaDoc throughout the entire codebase\n - There is a wiki with in-depth explanations and tutorials for most of owo's features over at https://docs.wispforest.io/owo/features\n\n## Features\n\nThis is by no means an exhaustive list, for a more complete overview head to https://docs.wispforest.io/owo/features\n\n - [owo-ui](https://docs.wispforest.io/owo/ui), a fully-featured declarative UI library for building dynamic, beautiful screens with blazingly fast development times\n - [owo-config](https://docs.wispforest.io/owo/config), a built-in, customizable configuration system built on top of owo-ui. It provides many of the same features as [Cloth Config](https://modrinth.com/mod/cloth-config) while many new conveniences, like server-client config synchronization, added on top\n - A fully automatic [registration system](https://docs.wispforest.io/owo/registration) that is designed to be as generic as possible. It is simple and non-verbose to use for basic registries, yet the underlying API tree is flexible and can also be used for many custom registration solutions\n - [Item Group extensions](https://docs.wispforest.io/owo/item-groups) which allow for sub-tabs inside your mod's group as well as a host of other features like custom buttons, textures and item variant handling\n - A fully-featured [networking layer](https://docs.wispforest.io/owo/networking) with fully automatic serialization, handshaking to ensure client compatibility and a built-in solution for triggering parametrized particle events in a side-agnostic manner\n - Client-sided particle helpers that allow for easily composing multi-particle effects\n - Rich text translations, allowing you to use Minecraft's text component format in your language files to provide styled text without any code\n"
  },
  {
    "path": "braid-reload-agent/.gitattributes",
    "content": "#\n# https://help.github.com/articles/dealing-with-line-endings/\n#\n# Linux start script should use lf\n/gradlew        text eol=lf\n\n# These are Windows script files and should use crlf\n*.bat           text eol=crlf\n\n"
  },
  {
    "path": "braid-reload-agent/.gitignore",
    "content": "# Ignore Gradle project-specific cache directory\n.gradle\n\n# Ignore Gradle build output directory\nbuild\n"
  },
  {
    "path": "braid-reload-agent/build.gradle.kts",
    "content": "plugins {\n    application\n    `maven-publish`\n}\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {}\n\nversion = \"0.1.0\"\ngroup = \"io.wispforest\"\n\njava {\n    toolchain {\n        languageVersion = JavaLanguageVersion.of(21)\n    }\n}\n\ntasks.jar {\n    manifest.attributes(\n        \"Premain-Class\" to \"io.wispforest.BraidReloadAgent\"\n    )\n}\n\npublishing {\n    publications {\n        create<MavenPublication>(\"maven\") {\n            from(components[\"java\"])\n        }\n    }\n\n    val env = System.getenv()\n    if (env.contains(\"MAVEN_URL\")) {\n        repositories {\n            maven {\n                url = uri(env[\"MAVEN_URL\"]!!)\n                credentials {\n                    username = env[\"MAVEN_USER\"]\n                    password = env[\"MAVEN_PASSWORD\"]\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "braid-reload-agent/gradle/libs.versions.toml",
    "content": "# This file was generated by the Gradle 'init' task.\n# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format\n\n[versions]\nguava = \"33.0.0-jre\"\njunit-jupiter = \"5.10.2\"\n\n[libraries]\nguava = { module = \"com.google.guava:guava\", version.ref = \"guava\" }\njunit-jupiter = { module = \"org.junit.jupiter:junit-jupiter\", version.ref = \"junit-jupiter\" }\n"
  },
  {
    "path": "braid-reload-agent/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.8-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "braid-reload-agent/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd \"${APP_HOME:-./}\" > /dev/null && pwd -P ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "braid-reload-agent/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "braid-reload-agent/settings.gradle.kts",
    "content": "plugins {\n    // Apply the foojay-resolver plugin to allow automatic download of JDKs\n    id(\"org.gradle.toolchains.foojay-resolver-convention\") version \"0.8.0\"\n}\n"
  },
  {
    "path": "braid-reload-agent/src/main/java/io/wispforest/BraidReloadAgent.java",
    "content": "package io.wispforest;\n\nimport java.lang.instrument.ClassFileTransformer;\nimport java.lang.instrument.IllegalClassFormatException;\nimport java.lang.instrument.Instrumentation;\nimport java.security.ProtectionDomain;\nimport java.util.*;\n\npublic class BraidReloadAgent {\n    public static void premain(String agentArgs, Instrumentation instrumentation) {\n        instrumentation.addTransformer(new RedefinitionListener());\n    }\n}\n\nclass RedefinitionListener implements ClassFileTransformer {\n\n    private final Map<String, Integer> classHashes = new HashMap<>();\n    private final Set<String> classesToWaitFor = new HashSet<>(Set.of(\n        \"io/wispforest/owo/braid/framework/widget/Widget\",\n        \"io/wispforest/owo/braid/framework/proxy/WidgetState\",\n        \"io/wispforest/owo/braid/core/BraidHotReloadCallback\"\n    ));\n\n    private ClassLoader braidClassLoader;\n    private boolean logSetupComplete = false;\n\n    @Override\n    public byte[] transform(Module module, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {\n        if (this.logSetupComplete) {\n            this.logSetupComplete = false;\n\n            fallible(() -> {\n                var callbackClass = Class.forName(\"io.wispforest.owo.braid.core.BraidHotReloadCallback\", false, this.braidClassLoader);\n                callbackClass.getMethod(\"setupComplete\").invoke(null);\n            });\n        }\n\n        if (!this.classesToWaitFor.isEmpty()) {\n            if (this.classesToWaitFor.contains(className)) {\n                this.classesToWaitFor.remove(className);\n\n                if (this.braidClassLoader == null) {\n                    this.braidClassLoader = loader;\n                }\n\n                if (this.classesToWaitFor.isEmpty()) {\n                    this.logSetupComplete = true;\n                }\n            }\n\n            return ClassFileTransformer.super.transform(module, loader, className, classBeingRedefined, protectionDomain, classfileBuffer);\n        }\n\n        fallible(() -> {\n            var widgetClass = Class.forName(\"io.wispforest.owo.braid.framework.widget.Widget\", false, this.braidClassLoader);\n            var widgetStateClass = Class.forName(\"io.wispforest.owo.braid.framework.proxy.WidgetState\", false, this.braidClassLoader);\n            var callbackClass = Class.forName(\"io.wispforest.owo.braid.core.BraidHotReloadCallback\", false, this.braidClassLoader);\n\n            if (classBeingRedefined != null) {\n                if (widgetClass.isAssignableFrom(classBeingRedefined) || widgetStateClass.isAssignableFrom(classBeingRedefined)) {\n                    var newHash = Arrays.hashCode(classfileBuffer);\n\n                    if (!this.classHashes.containsKey(className) || this.classHashes.get(className) != newHash) {\n                        callbackClass.getMethod(\"invoke\").invoke(null);\n                    }\n\n                    this.classHashes.put(className, newHash);\n                }\n            }\n        });\n\n        return ClassFileTransformer.super.transform(module, loader, className, classBeingRedefined, protectionDomain, classfileBuffer);\n    }\n\n    private static void fallible(Fallible fallible) {\n        fallible.run();\n    }\n}\n\ninterface Fallible {\n    void body() throws Throwable;\n\n    default void run() {\n        try {\n            this.body();\n        } catch (Throwable error) {\n            System.err.println(\"(braid reload agent) hotswap error: \" + error.getMessage());\n            //noinspection CallToPrintStackTrace\n            error.printStackTrace();\n        }\n    }\n}"
  },
  {
    "path": "build.gradle",
    "content": "//file:noinspection GradlePackageVersionRange\nplugins {\n    id 'net.fabricmc.fabric-loom-remap' version '1.15-SNAPSHOT'\n    id 'maven-publish'\n}\n\nallprojects {\n    apply plugin: \"java\"\n    apply plugin: \"fabric-loom\"\n    apply plugin: \"maven-publish\"\n\n    def ENV = System.getenv()\n\n    version = \"${project.mod_version}+${rootProject.minecraft_base_version}\"\n    group = rootProject.maven_group\n\n    base {\n        archivesName = project.archives_base_name\n    }\n\n    dependencies {\n        minecraft \"com.mojang:minecraft:${rootProject.minecraft_version}\"\n        mappings loom.officialMojangMappings()\n        modImplementation \"net.fabricmc:fabric-loader:${rootProject.loader_version}\"\n\n        modImplementation \"net.fabricmc.fabric-api:fabric-api:${project.fabric_version}\"\n    }\n\n    processResources {\n        inputs.property \"version\", project.version\n        filteringCharset \"UTF-8\"\n\n        filesMatching(\"fabric.mod.json\") {\n            expand \"version\": project.version\n        }\n    }\n\n    def targetJavaVersion = 21\n    tasks.withType(JavaCompile).configureEach {\n        // ensure that the encoding is set to UTF-8, no matter what the system default is\n        // this fixes some edge cases with special characters not displaying correctly\n        // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html\n        // If Javadoc is generated, this must be specified in that task too.\n        it.options.encoding = \"UTF-8\"\n        if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {\n            it.options.release = targetJavaVersion\n        }\n\n        options.compilerArgs << \"-Xmaxerrs\" << \"69420\"\n    }\n\n    java {\n        def javaVersion = JavaVersion.toVersion(targetJavaVersion)\n        if (JavaVersion.current() < javaVersion) {\n            toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)\n        }\n        // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the \"build\" task\n        // if it is present.\n        // If you remove this line, sources will not be generated.\n        withSourcesJar()\n    }\n\n    jar {\n        from(\"LICENSE\") {\n            rename { \"${it}_${project.base.archivesName.get()}\" }\n        }\n    }\n\n    publishing {\n        publications {\n            mavenJava(MavenPublication) {\n                from components.java\n            }\n        }\n\n        repositories {\n            maven {\n                url ENV.MAVEN_URL\n                credentials {\n                    username ENV.MAVEN_USER\n                    password ENV.MAVEN_PASSWORD\n                }\n            }\n        }\n    }\n}\n\nrepositories {\n    maven { url \"https://maven.terraformersmc.com/releases/\" }\n    maven { url \"https://maven.shedaniel.me/\" }\n    maven {\n        url \"https://api.modrinth.com/maven\"\n        content {\n            includeGroup \"maven.modrinth\"\n        }\n    }\n    maven { url \"https://maven.nucleoid.xyz/\" }\n    maven { url 'https://maven.wispforest.io' }\n    maven { url 'https://jitpack.io' }\n}\n\nsourceSets {\n    testmod {\n        runtimeClasspath += main.runtimeClasspath\n        compileClasspath += main.compileClasspath\n    }\n}\n\nloom {\n    runs {\n        testmodClient {\n            client()\n            ideConfigGenerated project.rootProject == project\n            name = \"Testmod Client\"\n            source sourceSets.testmod\n        }\n        testmodServer {\n            server()\n            ideConfigGenerated project.rootProject == project\n            name = \"Testmod Server\"\n            source sourceSets.testmod\n        }\n    }\n\n    accessWidenerPath = file(\"src/main/resources/owo.accesswidener\")\n}\n\ndependencies {\n//    modLocalRuntime(\"me.shedaniel:RoughlyEnoughItems-fabric:${project.rei_version}\")\n    modCompileOnly(\"me.shedaniel:RoughlyEnoughItems-default-plugin-fabric:${project.rei_version}\") {\n        exclude \"group\": \"net.fabricmc.fabric-api\"\n    }\n    modCompileOnly(\"me.shedaniel:RoughlyEnoughItems-api-fabric:${project.rei_version}\") {\n        exclude \"group\": \"net.fabricmc.fabric-api\"\n    }\n\n    modCompileOnly(\"dev.emi:emi-fabric:${project.emi_version}\") {\n        exclude \"group\": \"net.fabricmc.fabric-api\"\n    }\n//    modLocalRuntime(\"dev.emi:emi-fabric:${project.emi_version}\")\n\n    modCompileOnly(\"com.terraformersmc:modmenu:${project.modmenu_version}\")\n//    modLocalRuntime(\"com.terraformersmc:modmenu:${project.modmenu_version}\")\n\n    include api(\"io.wispforest:endec:0.1.12\")\n    include api(\"io.wispforest.endec:netty:0.1.6\")\n    include api(\"io.wispforest.endec:gson:0.1.7\")\n    include api(\"io.wispforest.endec:jankson:0.1.7\")\n\n    include api(\"blue.endless:jankson:${project.jankson_version}\")\n    include api(\"com.github.kdl-org:kdl4j:${project.kdl_version}\")\n\n    modCompileOnly(\"xyz.nucleoid:server-translations-api:${project.stapi_version}\")\n\n    testmodImplementation sourceSets.main.output\n    testmodAnnotationProcessor sourceSets.main.output\n}\n\njavadoc {\n    options.stylesheetFile = new File(projectDir, \"stylesheet.css\")\n    options.tags = [\"apiNote\", \"implNote\", \"implSpec\"]\n    options.addStringOption(\"Xdoclint:-missing\", \"-quiet\")\n    options.encoding = 'UTF-8'\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.2.1-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Done to increase the memory available to gradle.\norg.gradle.jvmargs=-Xmx2G\n# Fabric Properties\n# check these on https://fabricmc.net/develop\nminecraft_base_version=1.21.11\nminecraft_version=1.21.11\nyarn_mappings=1.21.11+build.2\nloader_version=0.18.2\n# Mod Properties\nmod_version=0.13.0\nmaven_group=io.wispforest\narchives_base_name=owo-lib\n# Dependencies\nfabric_version=0.141.2+1.21.11\n\n# https://maven.shedaniel.me/me/shedaniel/RoughlyEnoughItems-fabric/\nrei_version=21.9.812\n\n# https://maven.terraformersmc.com/releases/dev/emi/emi-fabric/\nemi_version=1.1.18+1.21.1\n\n# https://search.maven.org/artifact/blue.endless/jankson\njankson_version=1.2.2\n\n# https://jitpack.io/#kdl-org/kdl4j\nkdl_version=1.0.1\n\n# https://maven.terraformersmc.com/releases/com/terraformersmc/modmenu\nmodmenu_version=17.0.0-alpha.1\n\n# https://maven.nucleoid.xyz/xyz/nucleoid/server-translations-api/\nstapi_version=2.5.2+1.21.9-pre3\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    \n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "jitpack.yml",
    "content": "jdk:\n  - openjdk21"
  },
  {
    "path": "owo-sentinel/build.gradle",
    "content": "loom {\n    runConfigs.client.ideConfigGenerated = true\n\n    mods {\n        \"owo-sentinel\" {\n            sourceSet sourceSets.main\n        }\n    }\n}"
  },
  {
    "path": "owo-sentinel/gradle.properties",
    "content": "archives_base_name=owo-sentinel\n"
  },
  {
    "path": "owo-sentinel/src/main/java/io/wispforest/owosentinel/DownloadTask.java",
    "content": "package io.wispforest.owosentinel;\n\nimport javax.swing.*;\nimport java.util.function.Consumer;\n\npublic class DownloadTask extends SwingWorker<Void, Void> {\n\n    private final Runnable whenDone;\n    private final Consumer<String> logger;\n\n    public DownloadTask(Consumer<String> logger, Runnable whenDone) {\n        this.logger = logger;\n        this.whenDone = whenDone;\n    }\n\n    @Override\n    protected void done() {\n        whenDone.run();\n    }\n\n    @Override\n    protected Void doInBackground() {\n        try {\n            OwoSentinel.downloadAndInstall(logger);\n        } catch (Exception e) {\n            logger.accept(\"Download failed!\");\n            OwoSentinel.LOGGER.error(\"Download failed\", e);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "owo-sentinel/src/main/java/io/wispforest/owosentinel/Maldenhagen.java",
    "content": "package io.wispforest.owosentinel;\n\nimport net.fabricmc.loader.api.LanguageAdapter;\nimport net.fabricmc.loader.api.ModContainer;\n\npublic class Maldenhagen implements LanguageAdapter {\n    @Override\n    public <T> T create(ModContainer mod, String value, Class<T> type) {\n        throw new UnsupportedOperationException();\n    }\n\n    static {\n        OwoSentinel.launch();\n    }\n}\n"
  },
  {
    "path": "owo-sentinel/src/main/java/io/wispforest/owosentinel/OwoSentinel.java",
    "content": "package io.wispforest.owosentinel;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.fabricmc.loader.api.ModContainer;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.awt.*;\nimport java.io.InputStreamReader;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.function.Consumer;\n\npublic class OwoSentinel {\n    public static final Logger LOGGER = LogManager.getLogger(\"oωo-sentinel\");\n    private static final Gson GSON = new Gson();\n\n    public static final String OWO_EXPLANATION = \"\"\"\n            oωo-lib is a library used by most mods under the\n            Wisp Forest domain to ease development. This is\n            simply a convenient installer, as oωo is missing from your\n            installation. Should you not trust it, feel free to head to the\n            repository and download oωo yourself.\n            \"\"\";\n\n    public static final boolean FORCE_HEADLESS = Boolean.getBoolean(\"owo.sentinel.forceHeadless\");\n\n    public static void launch() {\n        if (FabricLoader.getInstance().isModLoaded(\"owo-impl\")) return;\n\n        try {\n            if (System.getProperty(\"os.name\").toLowerCase(Locale.ROOT).contains(\"mac\") || GraphicsEnvironment.isHeadless() || FORCE_HEADLESS) {\n                SentinelConsole.run();\n            } else {\n                SentinelWindow.open();\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"Error thrown while opening sentinel! Exiting\", e);\n            System.exit(1);\n        }\n\n        System.exit(0);\n    }\n\n    public static List<String> listOwoDependents() {\n        var list = new ArrayList<String>();\n        var used = new HashSet<String>();\n\n        for (var mod : FabricLoader.getInstance().getAllMods()) {\n            for (var dependency : mod.getMetadata().getDependencies()) {\n                if (!dependency.getModId().equals(\"owo\") && !dependency.getModId().equals(\"owo-lib\")) continue;\n                list.add(mod.getMetadata().getName() + \" (explicit dependency)\");\n                used.add(mod.getMetadata().getId());\n            }\n        }\n\n        FabricLoader.getInstance()\n                .getModContainer(\"owo-sentinel\")\n                .flatMap(ModContainer::getContainingMod)\n                .ifPresent(mod -> {\n                    if (used.contains(mod.getMetadata().getId())) return;\n\n                    list.add(mod.getMetadata().getName() + \" (included sentinel)\");\n                });\n\n        return list;\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    public static void downloadAndInstall(Consumer<String> logger) throws Exception {\n        logger.accept(\"Fetching versions\");\n        final URL url = new URL(\"https://api.modrinth.com/v2/project/owo-lib/version?game_versions=[%22\" + FabricLoader.getInstance().getRawGameVersion() + \"%22]&loaders=[%22fabric%22]\");\n\n        final var response = GSON.fromJson(new InputStreamReader(url.openStream()), JsonArray.class);\n\n        final var targetVersion = FabricLoader.getInstance().getModContainer(\"owo-sentinel\").orElseThrow().getMetadata().getVersion().getFriendlyString();\n\n        JsonObject latestVersion = null;\n\n        for (var version : response) {\n            final var versionObject = version.getAsJsonObject();\n\n            if (versionObject.get(\"version_number\").getAsString().equals(targetVersion)) {\n                latestVersion = versionObject;\n                break;\n            }\n        }\n\n        if (latestVersion != null) {\n            final var firstFile = latestVersion\n                    .get(\"files\").getAsJsonArray().get(0).getAsJsonObject();\n\n            final var versionUrl = firstFile\n                    .get(\"url\").getAsString();\n\n            final var versionFilename = firstFile\n                    .get(\"filename\").getAsString();\n\n            logger.accept(\"Found matching version: \" + latestVersion.get(\"version_number\").getAsString());\n\n            final var filePath = FabricLoader.getInstance().getGameDir().resolve(\"mods\").resolve(versionFilename);\n\n            logger.accept(\"Downloading...\");\n\n            try (final var modStream = new URL(versionUrl).openStream()) {\n                Files.copy(modStream, filePath, StandardCopyOption.REPLACE_EXISTING);\n            }\n\n            logger.accept(\"Success!\");\n        } else {\n            logger.accept(\"No matching version found\");\n        }\n    }\n}\n"
  },
  {
    "path": "owo-sentinel/src/main/java/io/wispforest/owosentinel/SentinelConsole.java",
    "content": "package io.wispforest.owosentinel;\n\nimport java.util.Locale;\nimport java.util.Scanner;\n\npublic class SentinelConsole {\n    public static void run() throws Exception {\n        System.out.println(\"oωo-lib is required to run the following mods:\");\n\n        for (String dependent : OwoSentinel.listOwoDependents()) {\n            System.out.println(\"- \" + dependent);\n        }\n\n        System.out.println(\"\\n\" + OwoSentinel.OWO_EXPLANATION);\n        System.out.print(\"Download and install (Y/n): \");\n\n        Scanner in = new Scanner(System.in);\n        boolean install = false;\n\n        try {\n            String answer = in.next();\n\n            install = answer.isBlank() || answer.toLowerCase(Locale.ROOT).startsWith(\"y\");\n        } catch (Exception e) {\n            System.out.println(\"<stdin blocked>\");\n        }\n\n        if (install) {\n            OwoSentinel.downloadAndInstall(System.out::println);\n        } else {\n            System.out.println(\"You can install oωo-lib at https://modrinth.com/mod/owo-lib.\");\n        }\n    }\n}\n"
  },
  {
    "path": "owo-sentinel/src/main/java/io/wispforest/owosentinel/SentinelWindow.java",
    "content": "package io.wispforest.owosentinel;\n\nimport javax.imageio.ImageIO;\nimport javax.swing.*;\nimport javax.swing.border.EmptyBorder;\nimport java.awt.*;\nimport java.awt.event.WindowAdapter;\nimport java.awt.event.WindowEvent;\nimport java.io.IOException;\nimport java.net.URI;\n\npublic class SentinelWindow {\n    public static void open() throws Exception {\n        // Fix AA\n        System.setProperty(\"awt.useSystemAAFontSettings\", \"lcd\");\n        System.setProperty(\"swing.aatext\", \"true\");\n\n        // Force GTK if available\n        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());\n        for (var laf : UIManager.getInstalledLookAndFeels()) {\n            if (!\"GTK+\".equals(laf.getName())) continue;\n            UIManager.setLookAndFeel(laf.getClassName());\n        }\n\n        // ------\n        // Window\n        // ------\n\n        JFrame window = new JFrame(\"oωo-sentinel\");\n        window.setVisible(false);\n\n        //noinspection ConstantConditions\n        final var owoIconImage = ImageIO.read(OwoSentinel.class.getClassLoader()\n                .getResourceAsStream(\"owo_sentinel_icon.png\"));\n\n        window.setIconImage(owoIconImage);\n        window.setMinimumSize(new Dimension(0, 250));\n        window.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);\n        window.addWindowListener(new WindowAdapter() {\n            @Override\n            public void windowClosed(WindowEvent e) {\n                System.exit(0);\n            }\n        });\n        window.setLocationByPlatform(true);\n\n        // -----\n        // Title\n        // -----\n\n        final var titleLabel = new JLabel(\"oωo-lib is required to run the following mods\", new ImageIcon(owoIconImage), SwingConstants.LEFT);\n        titleLabel.setFont(titleLabel.getFont().deriveFont(titleLabel.getFont().getSize() * 1.25f));\n        titleLabel.setHorizontalAlignment(SwingConstants.CENTER);\n        titleLabel.setBorder(new EmptyBorder(0, 15, 0, 15));\n        window.getContentPane().add(titleLabel, BorderLayout.NORTH);\n\n        // ----------\n        // Dependents\n        // ----------\n\n        var dependents = \"<html><center><b>\" + String.join(\"<br>\", OwoSentinel.listOwoDependents()) + \"<p>\\u200B\";\n\n        final var dependentsLabel = new JLabel(dependents);\n        final var defaultDepFont = dependentsLabel.getFont();\n\n        dependentsLabel.setFont(defaultDepFont.deriveFont(defaultDepFont.getSize() * 1.1f));\n        dependentsLabel.setHorizontalAlignment(SwingConstants.CENTER);\n\n        window.getContentPane().add(dependentsLabel, BorderLayout.CENTER);\n\n        // -------\n        // Buttons\n        // -------\n\n        var buttonsPanel = new JPanel();\n\n        // Download\n\n        final var downloadButton = new JButton(\"Download and install\");\n\n        final var progressBar = new JProgressBar();\n        progressBar.setIndeterminate(true);\n\n        downloadButton.addActionListener(e -> {\n            downloadButton.setEnabled(false);\n            downloadButton.add(progressBar);\n            downloadButton.updateUI();\n\n            titleLabel.setText(\"Installing oωo-lib\");\n            window.getContentPane().remove(dependentsLabel);\n\n            final var logBox = new JTextArea();\n            logBox.setEditable(false);\n            logBox.setMargin(new Insets(15, 15, 15, 15));\n            final var scrollPane = new JScrollPane(logBox);\n            scrollPane.setBorder(new EmptyBorder(0, 15, 0, 15));\n            window.getContentPane().add(scrollPane, BorderLayout.CENTER);\n\n            var task = new DownloadTask(s -> {\n                OwoSentinel.LOGGER.info(s);\n                logBox.setText(logBox.getText() + (logBox.getText().isBlank() ? \"\" : \"\\n\") + s);\n                scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum());\n            }, () -> {\n                progressBar.setVisible(false);\n                titleLabel.setText(\"\");\n                downloadButton.setText(\"Installed\");\n            });\n            task.execute();\n        });\n\n        // What is this\n\n        final var whatIsThisButton = new JButton(\"What is this?\");\n        whatIsThisButton.addActionListener(e -> {\n            String[] options = {\"Open GitHub\", \"OK\"};\n\n            int selection = JOptionPane.showOptionDialog(window, OwoSentinel.OWO_EXPLANATION, \"oωo-sentinel\",\n                    JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, new ImageIcon(owoIconImage),\n                    options, options[0]);\n\n            if (selection == 0 && Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {\n                try {\n                    Desktop.getDesktop().browse(URI.create(\"https://github.com/wisp-forest/owo-lib\"));\n                } catch (IOException ignored) {}\n            }\n        });\n\n        // Exit\n\n        final var exitButton = new JButton(\"Close\");\n        exitButton.addActionListener(e -> window.dispose());\n\n        // Panel setup\n\n        buttonsPanel.add(downloadButton);\n        buttonsPanel.add(whatIsThisButton);\n        buttonsPanel.add(exitButton);\n\n        // ---------------\n        // Window creation\n        // ---------------\n\n        window.getContentPane().add(buttonsPanel, BorderLayout.SOUTH);\n\n        window.pack();\n        window.setVisible(true);\n        window.requestFocus();\n\n        synchronized (SentinelWindow.class) {\n            SentinelWindow.class.wait();\n        }\n    }\n}\n"
  },
  {
    "path": "owo-sentinel/src/main/resources/fabric.mod.json",
    "content": "{\n  \"schemaVersion\": 1,\n  \"id\": \"owo-sentinel\",\n  \"version\": \"${version}\",\n  \"name\": \"oωo-sentinel\",\n  \"description\": \"makes u download oωo\",\n  \"authors\": [\n    \"glisco\"\n  ],\n  \"contact\": {},\n  \"license\": \"MIT\",\n  \"icon\": \"owo_sentinel_icon.png\",\n  \"environment\": \"*\",\n  \"provides\": [\n    \"owo\",\n    \"owo-lib\"\n  ],\n  \"languageAdapters\": {\n    \"maldenhagen\": \"io.wispforest.owosentinel.Maldenhagen\"\n  },\n  \"depends\": {\n    \"fabricloader\": \"*\",\n    \"minecraft\": \">=1.18\"\n  },\n  \"custom\": {\n    \"modmenu\": {\n      \"links\": {\n        \"modmenu.discord\": \"https://discord.gg/xrwHKktV2d\"\n      },\n      \"badges\": [\n        \"library\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "owo-ui.xsd",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"\n           xmlns:vc=\"http://www.w3.org/2007/XMLSchema-versioning\"\n           elementFormDefault=\"qualified\"\n           vc:minVersion=\"1.1\">\n\n    <xs:element name=\"owo-ui\">\n        <xs:complexType>\n            <xs:sequence>\n                <xs:element name=\"components\" minOccurs=\"0\">\n                    <xs:complexType>\n                        <xs:sequence>\n                            <xs:group ref=\"anyComponent\"/>\n                        </xs:sequence>\n                    </xs:complexType>\n                </xs:element>\n                <xs:element name=\"templates\" minOccurs=\"0\">\n                    <xs:complexType>\n                        <xs:sequence maxOccurs=\"unbounded\">\n                            <xs:choice>\n                                <xs:element name=\"template\">\n                                    <xs:complexType>\n                                        <xs:sequence>\n                                            <xs:group ref=\"anyComponent\"/>\n                                        </xs:sequence>\n                                        <xs:attribute name=\"name\" type=\"xs:Name\" use=\"required\"/>\n                                    </xs:complexType>\n                                </xs:element>\n                                <xs:any processContents=\"lax\"/>\n                            </xs:choice>\n                        </xs:sequence>\n                    </xs:complexType>\n                </xs:element>\n            </xs:sequence>\n        </xs:complexType>\n    </xs:element>\n\n    <xs:group name=\"anyComponent\">\n        <xs:choice>\n            <xs:element name=\"label\" type=\"owo-ui-label-component\"/>\n            <xs:element name=\"button\" type=\"owo-ui-button-component\"/>\n            <xs:element name=\"textured-button\" type=\"owo-ui-textured-button-component\"/>\n            <xs:element name=\"box\" type=\"owo-ui-box-component\"/>\n            <xs:element name=\"text-box\" type=\"owo-ui-text-field-component\"/>\n            <xs:element name=\"entity\" type=\"owo-ui-entity-component\"/>\n            <xs:element name=\"button\" type=\"owo-ui-button-component\"/>\n            <xs:element name=\"slider\" type=\"owo-ui-slider-component\"/>\n            <xs:element name=\"discrete-slider\" type=\"owo-ui-discrete-slider-component\"/>\n            <xs:element name=\"checkbox\" type=\"owo-ui-checkbox-component\"/>\n            <xs:element name=\"item\" type=\"owo-ui-item-component\"/>\n            <xs:element name=\"block\" type=\"owo-ui-block-component\"/>\n            <xs:element name=\"sprite\" type=\"owo-ui-sprite-component\"/>\n            <xs:element name=\"texture\" type=\"owo-ui-texture-component\"/>\n            <xs:element name=\"collapsible\" type=\"owo-ui-collapsible-container\"/>\n            <xs:element name=\"draggable\" type=\"owo-ui-draggable-container\"/>\n            <xs:element name=\"flow-layout\" type=\"owo-ui-flow-layout\"/>\n            <xs:element name=\"grid-layout\" type=\"owo-ui-grid-layout\"/>\n            <xs:element name=\"stack-layout\" type=\"owo-ui-stack-layout\"/>\n            <xs:element name=\"scroll\" type=\"owo-ui-scroll-container\"/>\n            <xs:element name=\"dropdown\" type=\"owo-ui-dropdown-component\"/>\n            <xs:element name=\"color-picker\" type=\"owo-ui-color-picker-component\"/>\n            <xs:element name=\"small-checkbox\" type=\"owo-ui-small-checkbox-component\"/>\n            <xs:element name=\"slim-slider\" type=\"owo-ui-slim-slider-component\"/>\n            <xs:element name=\"text-area\" type=\"owo-ui-text-area-component\"/>\n            <xs:element name=\"spacer\" type=\"owo-ui-spacer-component\"/>\n            <xs:element name=\"template\">\n                <xs:complexType>\n                    <xs:sequence minOccurs=\"0\" maxOccurs=\"unbounded\">\n                        <xs:any processContents=\"lax\"/>\n                    </xs:sequence>\n                    <xs:attribute name=\"name\" type=\"xs:string\" use=\"required\"/>\n                </xs:complexType>\n            </xs:element>\n            <xs:any processContents=\"lax\"/>\n        </xs:choice>\n    </xs:group>\n\n    <xs:complexType name=\"componentList\">\n        <xs:choice maxOccurs=\"unbounded\" minOccurs=\"0\">\n            <xs:group ref=\"anyComponent\"/>\n        </xs:choice>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-insets\">\n        <xs:annotation>\n            <xs:documentation>\n                Insets describing an offset on each side of a rectangle.\n                Elements which occur after one another override each other, meaning\n                that a `bottom` element after an `all` element will only redefine\n                the bottom offset and leave the rest intact\n            </xs:documentation>\n        </xs:annotation>\n\n        <xs:all>\n            <xs:element type=\"xs:integer\" name=\"top\" minOccurs=\"0\"/>\n            <xs:element type=\"xs:integer\" name=\"bottom\" minOccurs=\"0\"/>\n            <xs:element type=\"xs:integer\" name=\"left\" minOccurs=\"0\"/>\n            <xs:element type=\"xs:integer\" name=\"right\" minOccurs=\"0\"/>\n            <xs:element type=\"xs:integer\" name=\"all\" minOccurs=\"0\"/>\n            <xs:element type=\"xs:integer\" name=\"horizontal\" minOccurs=\"0\"/>\n            <xs:element type=\"xs:integer\" name=\"vertical\" minOccurs=\"0\"/>\n        </xs:all>\n    </xs:complexType>\n\n    <xs:simpleType name=\"positioningValueType\">\n        <xs:restriction base=\"xs:string\">\n            <xs:pattern value=\"-?\\d+,-?\\d+\"/>\n        </xs:restriction>\n    </xs:simpleType>\n\n    <xs:complexType name=\"owo-ui-positioning\">\n        <xs:annotation>\n            <xs:documentation>\n                Any of the three positioning types supported by owo-ui,\n                with the content formatted as `{horizontal},{vertical}`, eg `25,50`\n            </xs:documentation>\n        </xs:annotation>\n\n        <xs:simpleContent>\n            <xs:extension base=\"positioningValueType\">\n                <xs:attribute name=\"type\" use=\"required\">\n                    <xs:simpleType>\n                        <xs:restriction base=\"xs:string\">\n                            <xs:enumeration value=\"absolute\"/>\n                            <xs:enumeration value=\"relative\"/>\n                            <xs:enumeration value=\"layout\"/>\n                        </xs:restriction>\n                    </xs:simpleType>\n                </xs:attribute>\n            </xs:extension>\n        </xs:simpleContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"sizingDeclarationType\">\n        <xs:simpleContent>\n            <xs:extension base=\"xs:integer\">\n                <xs:attribute name=\"method\" use=\"required\">\n                    <xs:simpleType>\n                        <xs:restriction base=\"xs:string\">\n                            <xs:enumeration value=\"content\"/>\n                            <xs:enumeration value=\"fixed\"/>\n                            <xs:enumeration value=\"fill\"/>\n                            <xs:enumeration value=\"expand\"/>\n                        </xs:restriction>\n                    </xs:simpleType>\n                </xs:attribute>\n            </xs:extension>\n        </xs:simpleContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-sizing\">\n        <xs:annotation>\n            <xs:documentation>\n                A container for the horizontal and vertical sizing\n                declaration, each of which may occur once\n            </xs:documentation>\n        </xs:annotation>\n\n        <xs:all>\n            <xs:element name=\"horizontal\" type=\"sizingDeclarationType\" minOccurs=\"0\"/>\n            <xs:element name=\"vertical\" type=\"sizingDeclarationType\" minOccurs=\"0\"/>\n        </xs:all>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-text\">\n        <xs:annotation>\n            <xs:documentation>\n                Some literal or translated text, depending on whether\n                the `translate` attribute is `true`\n            </xs:documentation>\n        </xs:annotation>\n\n        <xs:simpleContent>\n            <xs:extension base=\"xs:string\">\n                <xs:attribute name=\"translate\">\n                    <xs:simpleType>\n                        <xs:restriction base=\"xs:boolean\"/>\n                    </xs:simpleType>\n                </xs:attribute>\n            </xs:extension>\n        </xs:simpleContent>\n    </xs:complexType>\n\n    <xs:simpleType name=\"owo-ui-color\">\n        <xs:annotation>\n            <xs:documentation>\n                A standard integer color in either `#AARRGGBB` or `#RRGGBB` format.\n                Alternatively, the all-lowercase name of any of Minecraft's 16 text colors\n            </xs:documentation>\n        </xs:annotation>\n\n        <xs:union>\n            <xs:simpleType>\n                <xs:restriction base=\"xs:string\">\n                    <xs:pattern value=\"#([A-Fa-f\\d]{2}){3,4}\"/>\n                </xs:restriction>\n            </xs:simpleType>\n\n            <xs:simpleType>\n                <xs:restriction base=\"xs:string\">\n                    <xs:enumeration value=\"black\"/>\n                    <xs:enumeration value=\"dark-blue\"/>\n                    <xs:enumeration value=\"dark-green\"/>\n                    <xs:enumeration value=\"dark-aqua\"/>\n                    <xs:enumeration value=\"dark-red\"/>\n                    <xs:enumeration value=\"dark-purple\"/>\n                    <xs:enumeration value=\"gold\"/>\n                    <xs:enumeration value=\"gray\"/>\n                    <xs:enumeration value=\"dark-gray\"/>\n                    <xs:enumeration value=\"blue\"/>\n                    <xs:enumeration value=\"green\"/>\n                    <xs:enumeration value=\"aqua\"/>\n                    <xs:enumeration value=\"red\"/>\n                    <xs:enumeration value=\"light-purple\"/>\n                    <xs:enumeration value=\"yellow\"/>\n                    <xs:enumeration value=\"white\"/>\n                </xs:restriction>\n            </xs:simpleType>\n        </xs:union>\n    </xs:simpleType>\n\n    <xs:simpleType name=\"owo-ui-vertical-alignment\">\n        <xs:restriction base=\"xs:string\">\n            <xs:enumeration value=\"top\"/>\n            <xs:enumeration value=\"center\"/>\n            <xs:enumeration value=\"bottom\"/>\n        </xs:restriction>\n    </xs:simpleType>\n\n    <xs:simpleType name=\"owo-ui-horizontal-alignment\">\n        <xs:restriction base=\"xs:string\">\n            <xs:enumeration value=\"left\"/>\n            <xs:enumeration value=\"center\"/>\n            <xs:enumeration value=\"right\"/>\n        </xs:restriction>\n    </xs:simpleType>\n\n    <xs:complexType name=\"owo-ui-surface\">\n        <xs:annotation>\n            <xs:documentation>\n                One or multiple surfaces chained together. If multiple surfaces\n                appear in this declaration, they are chained together in order of\n                appearance via the `and(...)` method\n            </xs:documentation>\n        </xs:annotation>\n\n        <xs:choice maxOccurs=\"unbounded\">\n            <xs:element name=\"panel\">\n                <xs:annotation>\n                    <xs:documentation>\n                        A standard Minecraft panel, optionally with a dark texture\n                    </xs:documentation>\n                </xs:annotation>\n                <xs:complexType>\n                    <xs:attribute name=\"dark\" type=\"xs:boolean\"/>\n                </xs:complexType>\n            </xs:element>\n            <xs:element name=\"panel-inset\">\n                <xs:annotation>\n                    <xs:documentation>\n                        An inset into a panel, used to create an area\n                        enclosed by a standard light panel\n                    </xs:documentation>\n                </xs:annotation>\n            </xs:element>\n            <xs:element name=\"panel-with-inset\" type=\"xs:unsignedInt\">\n                <xs:annotation>\n                    <xs:documentation>\n                        A panel inset bordered by a standard light panel\n                        of the specified width on each border\n                    </xs:documentation>\n                </xs:annotation>\n            </xs:element>\n            <xs:element name=\"tiled\">\n                <xs:annotation>\n                    <xs:documentation>\n                        A simple surface repeating the given texture, just like the\n                        options background does with the dirt texture\n                    </xs:documentation>\n                </xs:annotation>\n                <xs:complexType>\n                    <xs:simpleContent>\n                        <xs:extension base=\"minecraft-identifier\">\n                            <xs:attribute name=\"texture-width\" type=\"xs:unsignedInt\"/>\n                            <xs:attribute name=\"texture-height\" type=\"xs:unsignedInt\"/>\n                        </xs:extension>\n                    </xs:simpleContent>\n                </xs:complexType>\n            </xs:element>\n            <xs:element name=\"blur\">\n                <xs:annotation>\n                    <xs:documentation>\n                        A simple, colorless surface that blurs everything\n                        underneath itself\n                    </xs:documentation>\n                </xs:annotation>\n                <xs:complexType>\n                    <xs:attribute name=\"quality\" type=\"xs:float\" use=\"required\"/>\n                    <xs:attribute name=\"size\" type=\"xs:float\" use=\"required\"/>\n                </xs:complexType>\n            </xs:element>\n            <xs:element name=\"options-background\">\n                <xs:annotation>\n                    <xs:documentation>\n                        The standard Minecraft options background,\n                        usually a repeating dirt texture\n                    </xs:documentation>\n                </xs:annotation>\n            </xs:element>\n            <xs:element name=\"vanilla-translucent\">\n                <xs:annotation>\n                    <xs:documentation>\n                        The standard dark translucent background\n                        most Vanilla UIs use\n                    </xs:documentation>\n                </xs:annotation>\n            </xs:element>\n            <xs:element name=\"tooltip\">\n                <xs:annotation>\n                    <xs:documentation>\n                        The same renderer used by vanilla item\n                        and UI element tooltips\n                    </xs:documentation>\n                </xs:annotation>\n            </xs:element>\n            <xs:element type=\"owo-ui-color\" name=\"outline\">\n                <xs:annotation>\n                    <xs:documentation>\n                        A simple rectangular outline of the specified color\n                    </xs:documentation>\n                </xs:annotation>\n            </xs:element>\n            <xs:element type=\"owo-ui-color\" name=\"flat\">\n                <xs:annotation>\n                    <xs:documentation>\n                        A flat rectangle of the specified color\n                    </xs:documentation>\n                </xs:annotation>\n            </xs:element>\n        </xs:choice>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-scrollbar\">\n        <xs:choice>\n            <xs:element type=\"owo-ui-color\" name=\"flat\"/>\n            <xs:element name=\"vanilla\"/>\n            <xs:element name=\"vanilla-flat\"/>\n        </xs:choice>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-dropdown-entries\">\n        <xs:choice maxOccurs=\"unbounded\">\n            <xs:element name=\"divider\"/>\n            <xs:element type=\"owo-ui-text\" name=\"text\"/>\n            <xs:element name=\"button\">\n                <xs:complexType>\n                    <xs:sequence>\n                        <xs:element type=\"owo-ui-text\" name=\"text\"/>\n                    </xs:sequence>\n                </xs:complexType>\n            </xs:element>\n            <xs:element name=\"checkbox\">\n                <xs:complexType>\n                    <xs:sequence>\n                        <xs:element type=\"owo-ui-text\" name=\"text\"/>\n                        <xs:element type=\"xs:boolean\" name=\"checked\"/>\n                    </xs:sequence>\n                </xs:complexType>\n            </xs:element>\n            <xs:element name=\"nested\">\n                <xs:complexType>\n                    <xs:complexContent>\n                        <xs:extension base=\"owo-ui-dropdown-entries\">\n                            <xs:attribute type=\"xs:string\" name=\"name\"/>\n                            <xs:attribute type=\"xs:boolean\" name=\"translate\"/>\n                        </xs:extension>\n                    </xs:complexContent>\n                </xs:complexType>\n            </xs:element>\n        </xs:choice>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-button-renderer\">\n        <xs:choice>\n            <xs:element name=\"vanilla\"/>\n            <xs:element name=\"flat\">\n                <xs:complexType>\n                    <xs:attribute type=\"owo-ui-color\" name=\"color\" use=\"required\"/>\n                    <xs:attribute type=\"owo-ui-color\" name=\"hovered-color\" use=\"required\"/>\n                    <xs:attribute type=\"owo-ui-color\" name=\"disabled-color\" use=\"required\"/>\n                </xs:complexType>\n            </xs:element>\n            <xs:element name=\"texture\">\n                <xs:complexType>\n                    <xs:attribute type=\"minecraft-identifier\" name=\"texture\" use=\"required\"/>\n                    <xs:attribute type=\"xs:unsignedInt\" name=\"u\" use=\"required\"/>\n                    <xs:attribute type=\"xs:unsignedInt\" name=\"v\" use=\"required\"/>\n                    <xs:attribute type=\"xs:unsignedInt\" name=\"texture-width\" use=\"required\"/>\n                    <xs:attribute type=\"xs:unsignedInt\" name=\"texture-height\" use=\"required\"/>\n                </xs:complexType>\n            </xs:element>\n        </xs:choice>\n    </xs:complexType>\n\n    <xs:simpleType name=\"owo-ui-axis-direction\">\n        <xs:restriction base=\"xs:string\">\n            <xs:enumeration value=\"vertical\"/>\n            <xs:enumeration value=\"horizontal\"/>\n        </xs:restriction>\n    </xs:simpleType>\n\n    <xs:simpleType name=\"minecraft-identifier\">\n        <xs:annotation>\n            <xs:documentation>\n                A standard Minecraft identifier, optionally with the\n                namespace omitted and defaulted to `minecraft`\n            </xs:documentation>\n        </xs:annotation>\n        <xs:restriction base=\"xs:string\">\n            <xs:pattern value=\"([a-z0-9_.-]+:)?[a-z0-9/._-]+\"/>\n        </xs:restriction>\n    </xs:simpleType>\n\n    <xs:group name=\"componentProps\">\n        <xs:choice>\n            <xs:element type=\"owo-ui-insets\" name=\"margins\" minOccurs=\"0\"/>\n            <xs:element type=\"owo-ui-positioning\" name=\"positioning\" minOccurs=\"0\"/>\n            <xs:element type=\"owo-ui-sizing\" name=\"sizing\" minOccurs=\"0\"/>\n            <xs:element type=\"owo-ui-text\" name=\"tooltip-text\" minOccurs=\"0\"/>\n            <xs:element name=\"cursor-style\" minOccurs=\"0\">\n                <xs:simpleType>\n                    <xs:restriction base=\"xs:string\">\n                        <xs:enumeration value=\"pointer\"/>\n                        <xs:enumeration value=\"text\"/>\n                        <xs:enumeration value=\"hand\"/>\n                        <xs:enumeration value=\"move\"/>\n                    </xs:restriction>\n                </xs:simpleType>\n            </xs:element>\n        </xs:choice>\n    </xs:group>\n\n    <xs:group name=\"parentComponentProps\">\n        <xs:choice>\n            <xs:group ref=\"componentProps\" maxOccurs=\"unbounded\"/>\n            <xs:element type=\"owo-ui-insets\" name=\"padding\" minOccurs=\"0\"/>\n            <xs:element type=\"owo-ui-surface\" name=\"surface\" minOccurs=\"0\"/>\n            <xs:element type=\"owo-ui-horizontal-alignment\" name=\"horizontal-alignment\" minOccurs=\"0\"/>\n            <xs:element type=\"owo-ui-vertical-alignment\" name=\"vertical-alignment\" minOccurs=\"0\"/>\n            <xs:element type=\"xs:boolean\" name=\"allow-overflow\" minOccurs=\"0\"/>\n        </xs:choice>\n    </xs:group>\n\n    <xs:group name=\"vanillaWidgetProps\">\n        <xs:choice>\n            <xs:group ref=\"parentComponentProps\" maxOccurs=\"unbounded\"/>\n            <xs:element type=\"xs:boolean\" name=\"active\" minOccurs=\"0\"/>\n        </xs:choice>\n    </xs:group>\n\n    <xs:complexType name=\"componentType\">\n        <xs:attribute name=\"id\"/>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-label-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element type=\"owo-ui-text\" name=\"text\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"max-width\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"line-height\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"line-spacing\" minOccurs=\"0\"/>\n                    <xs:element type=\"owo-ui-color\" name=\"color\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"shadow\" minOccurs=\"0\"/>\n                    <xs:element type=\"owo-ui-vertical-alignment\" name=\"vertical-text-alignment\" minOccurs=\"0\"/>\n                    <xs:element type=\"owo-ui-horizontal-alignment\" name=\"horizontal-text-alignment\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-spacer-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:group ref=\"componentProps\"/>\n                <xs:attribute name=\"percent\" type=\"xs:unsignedInt\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-box-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:choice minOccurs=\"0\">\n                        <xs:element type=\"owo-ui-color\" name=\"color\"/>\n                        <xs:sequence>\n                            <xs:element type=\"owo-ui-color\" name=\"start-color\"/>\n                            <xs:element type=\"owo-ui-color\" name=\"end-color\"/>\n                        </xs:sequence>\n                    </xs:choice>\n                    <xs:element type=\"xs:boolean\" name=\"fill\" minOccurs=\"0\"/>\n                    <xs:element name=\"direction\" minOccurs=\"0\">\n                        <xs:simpleType>\n                            <xs:restriction base=\"xs:string\">\n                                <xs:enumeration value=\"top-to-bottom\"/>\n                                <xs:enumeration value=\"left-to-right\"/>\n                                <xs:enumeration value=\"right-to-left\"/>\n                                <xs:enumeration value=\"bottom-to-top\"/>\n                            </xs:restriction>\n                        </xs:simpleType>\n                    </xs:element>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-checkbox-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"vanillaWidgetProps\"/>\n                    <xs:element type=\"xs:boolean\" name=\"checked\" minOccurs=\"0\"/>\n                    <xs:element type=\"owo-ui-text\" name=\"text\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-button-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"vanillaWidgetProps\"/>\n                    <xs:element type=\"owo-ui-text\" name=\"text\" minOccurs=\"0\"/>\n                    <xs:element type=\"owo-ui-button-renderer\" name=\"renderer\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-textured-button-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"vanillaWidgetProps\"/>\n                    <xs:element type=\"owo-ui-text\" name=\"text\" minOccurs=\"0\"/>\n                </xs:choice>\n                <xs:attribute name=\"texture\" type=\"minecraft-identifier\" use=\"required\"/>\n                <xs:attribute name=\"width\" type=\"xs:integer\" use=\"required\"/>\n                <xs:attribute name=\"height\" type=\"xs:integer\" use=\"required\"/>\n                <xs:attribute name=\"u\" type=\"xs:integer\"/>\n                <xs:attribute name=\"v\" type=\"xs:integer\"/>\n                <xs:attribute name=\"texture-width\" type=\"xs:integer\"/>\n                <xs:attribute name=\"texture-height\" type=\"xs:integer\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-text-field-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"vanillaWidgetProps\"/>\n                    <xs:element type=\"xs:string\" name=\"text\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"max-length\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"show-background\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-text-area-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"vanillaWidgetProps\"/>\n                    <xs:element type=\"xs:string\" name=\"text\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"max-length\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"max-lines\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"display-char-count\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-color-picker-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"selector-width\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"selector-padding\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"show-alpha\" minOccurs=\"0\"/>\n                    <xs:element type=\"owo-ui-color\" name=\"selected-color\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-small-checkbox-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element type=\"owo-ui-text\" name=\"label\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"label-shadow\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"checked\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-slim-slider-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element type=\"xs:double\" name=\"step-size\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:double\" name=\"min\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:double\" name=\"max\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:double\" name=\"value\" minOccurs=\"0\"/>\n                </xs:choice>\n                <xs:attribute name=\"direction\" type=\"owo-ui-axis-direction\" use=\"required\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-slider-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"vanillaWidgetProps\"/>\n                    <xs:element type=\"owo-ui-text\" name=\"text\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:double\" name=\"value\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-discrete-slider-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"vanillaWidgetProps\"/>\n                    <xs:element type=\"xs:unsignedInt\" name=\"decimal-places\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:double\" name=\"value\" minOccurs=\"0\"/>\n                </xs:choice>\n                <xs:attribute name=\"min\" type=\"xs:double\" use=\"required\"/>\n                <xs:attribute name=\"max\" type=\"xs:double\" use=\"required\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-entity-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element type=\"xs:float\" name=\"scale\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"look-at-cursor\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"mouse-rotation\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"scale-to-fit\" minOccurs=\"0\"/>\n                </xs:choice>\n                <xs:attribute name=\"type\" type=\"xs:string\" use=\"required\"/>\n                <xs:attribute name=\"nbt\" type=\"xs:string\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-item-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element type=\"xs:string\" name=\"stack\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"show-overlay\" minOccurs=\"0\"/>\n                    <xs:element type=\"xs:boolean\" name=\"set-tooltip-from-stack\" minOccurs=\"0\"/>\n                </xs:choice>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-block-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:group ref=\"componentProps\"/>\n                <xs:attribute name=\"state\" use=\"required\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-sprite-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice>\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element name=\"blend\" type=\"xs:boolean\" minOccurs=\"0\"/>\n                </xs:choice>\n                <xs:attribute name=\"atlas\" type=\"minecraft-identifier\" use=\"required\"/>\n                <xs:attribute name=\"sprite\" type=\"minecraft-identifier\" use=\"required\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-texture-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:choice maxOccurs=\"unbounded\">\n                    <xs:group ref=\"componentProps\"/>\n                    <xs:element name=\"blend\" type=\"xs:boolean\" minOccurs=\"0\"/>\n                    <xs:element name=\"visible-area\" minOccurs=\"0\">\n                        <xs:complexType>\n                            <xs:all>\n                                <xs:element name=\"x\" type=\"xs:unsignedInt\" minOccurs=\"0\"/>\n                                <xs:element name=\"y\" type=\"xs:unsignedInt\" minOccurs=\"0\"/>\n                                <xs:element name=\"width\" type=\"xs:unsignedInt\" minOccurs=\"0\"/>\n                                <xs:element name=\"height\" type=\"xs:unsignedInt\" minOccurs=\"0\"/>\n                            </xs:all>\n                        </xs:complexType>\n                    </xs:element>\n                </xs:choice>\n                <xs:attribute name=\"texture\" type=\"minecraft-identifier\" use=\"required\"/>\n                <xs:attribute name=\"u\" type=\"xs:integer\"/>\n                <xs:attribute name=\"v\" type=\"xs:integer\"/>\n                <xs:attribute name=\"region-width\" type=\"xs:integer\"/>\n                <xs:attribute name=\"region-height\" type=\"xs:integer\"/>\n                <xs:attribute name=\"texture-width\" type=\"xs:integer\"/>\n                <xs:attribute name=\"texture-height\" type=\"xs:integer\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-dropdown-component\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:sequence>\n                    <xs:choice maxOccurs=\"unbounded\">\n                        <xs:group ref=\"parentComponentProps\"/>\n                        <xs:element type=\"xs:boolean\" name=\"close-when-not-hovered\" minOccurs=\"0\"/>\n                        <xs:element type=\"owo-ui-dropdown-entries\" name=\"entries\" minOccurs=\"0\"/>\n                    </xs:choice>\n                </xs:sequence>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-flow-layout\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:sequence>\n                    <xs:element type=\"componentList\" name=\"children\"/>\n                    <xs:choice maxOccurs=\"unbounded\">\n                        <xs:element type=\"xs:integer\" name=\"gap\" minOccurs=\"0\"/>\n                        <xs:group ref=\"parentComponentProps\" maxOccurs=\"unbounded\"/>\n                    </xs:choice>\n                </xs:sequence>\n                <xs:attribute name=\"direction\" use=\"required\">\n                    <xs:simpleType>\n                        <xs:restriction base=\"xs:string\">\n                            <xs:enumeration value=\"vertical\"/>\n                            <xs:enumeration value=\"horizontal\"/>\n                            <xs:enumeration value=\"ltr-text-flow\"/>\n                        </xs:restriction>\n                    </xs:simpleType>\n                </xs:attribute>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-grid-layout\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:sequence>\n                    <xs:element type=\"componentList\" name=\"children\"/>\n                    <xs:group ref=\"parentComponentProps\" maxOccurs=\"unbounded\"/>\n                </xs:sequence>\n                <xs:attribute name=\"rows\" type=\"xs:unsignedInt\" use=\"required\"/>\n                <xs:attribute name=\"columns\" type=\"xs:unsignedInt\" use=\"required\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-stack-layout\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:sequence>\n                    <xs:element type=\"componentList\" name=\"children\"/>\n                    <xs:group ref=\"parentComponentProps\" maxOccurs=\"unbounded\"/>\n                </xs:sequence>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-collapsible-container\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:sequence>\n                    <xs:element type=\"componentList\" name=\"children\"/>\n                    <xs:choice maxOccurs=\"unbounded\">\n                        <xs:group ref=\"parentComponentProps\"/>\n                        <xs:element type=\"owo-ui-text\" name=\"text\" minOccurs=\"0\"/>\n                    </xs:choice>\n                </xs:sequence>\n                <xs:attribute name=\"expanded\" type=\"xs:boolean\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-draggable-container\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:sequence>\n                    <xs:group ref=\"anyComponent\"/>\n                    <xs:choice maxOccurs=\"unbounded\">\n                        <xs:group ref=\"parentComponentProps\"/>\n                        <xs:element type=\"xs:unsignedInt\" name=\"forehead-size\" minOccurs=\"0\"/>\n                    </xs:choice>\n                </xs:sequence>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n\n    <xs:complexType name=\"owo-ui-scroll-container\">\n        <xs:complexContent>\n            <xs:extension base=\"componentType\">\n                <xs:sequence>\n                    <xs:group ref=\"anyComponent\"/>\n                    <xs:choice maxOccurs=\"unbounded\">\n                        <xs:group ref=\"parentComponentProps\"/>\n                        <xs:element type=\"xs:unsignedInt\" name=\"scrollbar-thiccness\" minOccurs=\"0\"/>\n                        <xs:element type=\"xs:unsignedInt\" name=\"fixed-scrollbar-length\" minOccurs=\"0\"/>\n                        <xs:element type=\"owo-ui-color\" name=\"scrollbar-color\" minOccurs=\"0\"/>\n                        <xs:element type=\"owo-ui-scrollbar\" name=\"scrollbar\" minOccurs=\"0\"/>\n                    </xs:choice>\n                </xs:sequence>\n                <xs:attribute name=\"direction\" type=\"owo-ui-axis-direction\" use=\"required\"/>\n            </xs:extension>\n        </xs:complexContent>\n    </xs:complexType>\n</xs:schema>"
  },
  {
    "path": "settings.gradle",
    "content": "pluginManagement {\n    repositories {\n        maven {\n            name = 'Fabric'\n            url = 'https://maven.fabricmc.net/'\n        }\n        gradlePluginPortal()\n    }\n}\n\ninclude 'owo-sentinel'"
  },
  {
    "path": "src/main/java/io/wispforest/owo/Owo.java",
    "content": "package io.wispforest.owo;\n\nimport io.wispforest.owo.client.screens.MenuNetworkingInternals;\nimport io.wispforest.owo.command.debug.OwoDebugCommands;\nimport io.wispforest.owo.ops.LootOps;\nimport io.wispforest.owo.text.CustomTextRegistry;\nimport io.wispforest.owo.text.InsertingTextContent;\nimport io.wispforest.owo.util.Wisdom;\nimport net.fabricmc.api.ModInitializer;\nimport net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.MinecraftServer;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport static io.wispforest.owo.ops.TextOps.withColor;\n\npublic class Owo implements ModInitializer {\n\n    public static final String MOD_ID = \"owo\";\n    /**\n     * Whether oωo debug is enabled, this defaults to {@code true} in a development environment.\n     * To override that behavior, add the {@code -Dowo.debug=false} java argument\n     */\n    public static final boolean DEBUG;\n    public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);\n    private static MinecraftServer SERVER;\n\n    public static final Component PREFIX = Component.empty().withStyle(ChatFormatting.GRAY)\n        .append(withColor(\"o\", 0x3955e5))\n        .append(withColor(\"ω\", 0x13a6f0))\n        .append(withColor(\"o\", 0x3955e5))\n        .append(Component.literal(\" > \").withStyle(ChatFormatting.GRAY));\n\n    static {\n        boolean debug = FabricLoader.getInstance().isDevelopmentEnvironment();\n        if (System.getProperty(\"owo.debug\") != null) debug = Boolean.getBoolean(\"owo.debug\");\n        if (Boolean.getBoolean(\"owo.forceDisableDebug\")) {\n            LOGGER.warn(\"Deprecated system property 'owo.forceDisableDebug=true' was used - use 'owo.debug=false' instead\");\n            debug = false;\n        }\n\n        DEBUG = debug;\n    }\n\n    @Override\n    @ApiStatus.Internal\n    public void onInitialize() {\n        LootOps.registerListener();\n        CustomTextRegistry.register(\"index\", InsertingTextContent.CODEC);\n        MenuNetworkingInternals.init();\n\n        ServerLifecycleEvents.SERVER_STARTING.register(server -> SERVER = server);\n        ServerLifecycleEvents.SERVER_STOPPED.register(server -> SERVER = null);\n\n        Wisdom.spread();\n\n        if (!DEBUG) return;\n\n        OwoDebugCommands.register();\n    }\n\n    @ApiStatus.Internal\n    public static void debugWarn(Logger logger, String message) {\n        if (!DEBUG) return;\n        logger.warn(message);\n    }\n\n    @ApiStatus.Internal\n    public static void debugWarn(Logger logger, String message, Object... params) {\n        if (!DEBUG) return;\n        logger.warn(message, params);\n    }\n\n    /**\n     * @return The currently active minecraft server instance. If running\n     * on a physical client, this will return the integrated server while in\n     * a local singleplayer world and {@code null} otherwise\n     */\n    public static MinecraftServer currentServer() {\n        return SERVER;\n    }\n\n    // \"eh it's only like 10-15 of them what's the big deal\" - glisco, while writing the 52nd hardcoded Identifier.of(\"owo\", ...)\n    @ApiStatus.Internal\n    public static Identifier id(String path) {\n        return Identifier.fromNamespaceAndPath(MOD_ID, path);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/blockentity/LinearProcess.java",
    "content": "package io.wispforest.owo.blockentity;\n\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;\nimport net.minecraft.world.level.Level;\n\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\n\n/**\n * Represents a process made of steps than can be executed tick by tick using a respective\n * {@link LinearProcessExecutor}. This can, for example, be used on BlockEntities that perform\n * rituals or similar activities that are made of consecutive steps.\n * <p>\n * A process defines the pattern of steps and events that shall be followed, thus there is one (usually static)\n * instance of it. You then create a new instance of {@link LinearProcessExecutor} using the\n * {@link #createExecutor(Object)} method for each instance of your BlockEntity of whatever else if supposed to run it\n * <p>\n * To create a new process, call {@link #LinearProcess(int)} with the length it should have. A process always has the same\n * length. Then, in the constructor of each object that will use an executor, use {@link #createExecutor(Object)} to\n * obtain an instance. This then has to be told whether it lives on the client or server using\n * {@link #configureExecutor(LinearProcessExecutor, boolean)}. On a BlockEntity this can be achieved by overriding\n * {@link net.minecraft.world.level.block.entity.BlockEntity#setLevel(Level)} and configuring after the super call using the provided\n * world\n * <p>\n * Steps and events should be added to process once, ideally in the {@code static} initializer block of the containing class.\n * After the process is complete, call {@link #finish()} to prevent further changes\n *\n * @param <T> The type of object this process will be executed on,\n *            a {@link net.minecraft.world.level.block.entity.BlockEntity} in most cases\n */\npublic class LinearProcess<T> {\n\n    private final Int2ObjectMap<BiConsumer<LinearProcessExecutor<T>, T>> clientEventTable = new Int2ObjectOpenHashMap<>();\n    private final Int2ObjectMap<LinearProcessExecutor.ProcessStep<T>> clientProcessStepTable = new Int2ObjectOpenHashMap<>();\n\n    private final Int2ObjectMap<BiConsumer<LinearProcessExecutor<T>, T>> serverEventTable = new Int2ObjectOpenHashMap<>();\n    private final Int2ObjectMap<LinearProcessExecutor.ProcessStep<T>> serverProcessStepTable = new Int2ObjectOpenHashMap<>();\n\n    private Predicate<LinearProcessExecutor<T>> condition = tLinearProcessExecutor -> true;\n\n    private final int processLength;\n    private boolean finished = false;\n\n    /**\n     * Creates a new process\n     *\n     * @param processLength The length of the process. This is immutable\n     */\n    public LinearProcess(int processLength) {\n        this.processLength = processLength;\n    }\n\n    /**\n     * Creates a new executor for the given target object\n     *\n     * @param target The object the executor should operate on\n     * @return The created executor. This is not ready for use yet\n     * @see #configureExecutor(LinearProcessExecutor, boolean)\n     */\n    public LinearProcessExecutor<T> createExecutor(T target) {\n        if (!finished) throw new IllegalStateException(\"Illegal attempt to create executor for unfinished process\");\n        return new LinearProcessExecutor<>(target, processLength, condition, serverProcessStepTable);\n    }\n\n    /**\n     * Configures an executor to use either the\n     * server or client instructions\n     *\n     * @param executor The executor to configure\n     * @param client   {@code true} if the client instructions should be used\n     */\n    public void configureExecutor(LinearProcessExecutor<T> executor, boolean client) {\n        if (!finished) throw new IllegalStateException(\"Illegal attempt to configure executor using unfinished process\");\n\n        if (client) {\n            executor.configure(clientEventTable, clientProcessStepTable);\n        } else {\n            executor.configure(serverEventTable, serverProcessStepTable);\n        }\n    }\n\n    /**\n     * Adds a new step to this process on both client and server\n     *\n     * @param when     When the step should start\n     * @param length   How long it should last\n     * @param executor The code to be run each tick while the step is active\n     */\n    public void addCommonStep(int when, int length, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        checkForIllegalModification();\n        var step = new LinearProcessExecutor.ProcessStep<>(length, executor);\n        clientProcessStepTable.put(when, step);\n        serverProcessStepTable.put(when, step);\n    }\n\n    /**\n     * @see #addCommonStep(int, int, BiConsumer)\n     */\n    public void addClientStep(int when, int length, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        checkForIllegalModification();\n        var step = new LinearProcessExecutor.ProcessStep<>(length, executor);\n        clientProcessStepTable.put(when, step);\n    }\n\n    /**\n     * @see #addCommonStep(int, int, BiConsumer)\n     */\n    public void addServerStep(int when, int length, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        checkForIllegalModification();\n        var step = new LinearProcessExecutor.ProcessStep<>(length, executor);\n        serverProcessStepTable.put(when, step);\n    }\n\n    /**\n     * Adds an event that is executed once, on both client and server\n     *\n     * @param when     When the event should occur\n     * @param executor The code to be run on the given tick\n     * @see #addClientEvent(int, BiConsumer)\n     * @see #addServerEvent(int, BiConsumer)\n     */\n    public void addCommonEvent(int when, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(when, clientEventTable, executor);\n        eventAtIndex(when, serverEventTable, executor);\n    }\n\n    /**\n     * @see #addCommonEvent(int, BiConsumer)\n     */\n    public void addClientEvent(int when, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(when, clientEventTable, executor);\n    }\n\n    /**\n     * @see #addCommonEvent(int, BiConsumer)\n     */\n    public void addServerEvent(int when, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(when, serverEventTable, executor);\n    }\n\n    /**\n     * Defines code to be run when this process has successfully\n     * finished, on both client and server\n     *\n     * @param executor The code to be run\n     * @see #whenFinishedClient(BiConsumer)\n     * @see #whenFinishedServer(BiConsumer)\n     */\n    public void whenFinishedCommon(BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, clientEventTable, executor);\n        eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, serverEventTable, executor);\n    }\n\n    /**\n     * @see #whenFinishedCommon(BiConsumer)\n     */\n    public void whenFinishedServer(BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, serverEventTable, executor);\n    }\n\n    /**\n     * @see #whenFinishedCommon(BiConsumer)\n     */\n    public void whenFinishedClient(BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, clientEventTable, executor);\n    }\n\n    /**\n     * Defines code to be run on both client and server when this process\n     * is unexpectedly cancelled mid-execution, use this to clean up after you.\n     *\n     * @param executor The code to be run\n     * @see #onCancelledClient(BiConsumer)\n     * @see #onCancelledServer(BiConsumer)\n     */\n    public void onCancelledCommon(BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, clientEventTable, executor);\n        eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, serverEventTable, executor);\n    }\n\n    /**\n     * @see #onCancelledCommon(BiConsumer)\n     */\n    public void onCancelledServer(BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, serverEventTable, executor);\n    }\n\n    /**\n     * @see #onCancelledCommon(BiConsumer)\n     */\n    public void onCancelledClient(BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, clientEventTable, executor);\n    }\n\n    /**\n     * Defines a condition that has to be met every tick this process runs,\n     * otherwise it cancels itself\n     *\n     * @param condition The condition that should be satisfied during the entire\n     *                  process execution\n     */\n    public void runConditionally(Predicate<LinearProcessExecutor<T>> condition) {\n        this.condition = condition;\n    }\n\n    /**\n     * Marks this process and completely built and ready for execution\n     */\n    public void finish() {\n        this.finished = true;\n    }\n\n    private void checkForIllegalModification() {\n        if (finished) throw new IllegalStateException(\"Illegal attempt to modify finished process\");\n    }\n\n    private void eventAtIndex(int index, Int2ObjectMap<BiConsumer<LinearProcessExecutor<T>, T>> eventTable, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n        checkForIllegalModification();\n        eventTable.put(index, executor);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/blockentity/LinearProcessExecutor.java",
    "content": "package io.wispforest.owo.blockentity;\n\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport net.minecraft.nbt.CompoundTag;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\nimport java.util.function.Predicate;\n\n/**\n * A handler that executes the steps defined in a {@link LinearProcess}. Each object that is\n * supposed to run the process needs an instance of this, and each instance of this refers back\n * to the object it operates on\n *\n * @param <T> The type of object this executor operates on\n */\npublic class LinearProcessExecutor<T> {\n\n    public static final int CANCEL_EVENT_INDEX = -1;\n    public static final int FINISH_EVENT_INDEX = -2;\n\n    private final T target;\n    private final int processLength;\n\n    private final Predicate<LinearProcessExecutor<T>> condition;\n    private Int2ObjectMap<BiConsumer<LinearProcessExecutor<T>, T>> eventTable;\n    private Int2ObjectMap<ProcessStep<T>> processStepTable;\n\n    private final Set<ProcessStep.Info<T>> activeSteps = new HashSet<>();\n\n    private int processTick = 0;\n\n    protected LinearProcessExecutor(T target, int processLength, Predicate<LinearProcessExecutor<T>> condition, Int2ObjectMap<ProcessStep<T>> serverStepTable) {\n        this.target = target;\n        this.processLength = processLength;\n        this.condition = condition;\n        this.eventTable = null;\n        this.processStepTable = serverStepTable;\n    }\n\n    protected void configure(Int2ObjectMap<BiConsumer<LinearProcessExecutor<T>, T>> eventTable, Int2ObjectMap<ProcessStep<T>> processStepTable) {\n        this.eventTable = eventTable;\n        this.processStepTable = processStepTable;\n    }\n\n    public void tick() {\n        if (this.eventTable == null) throw new IllegalStateException(\"Illegal attempt to tick unconfigured executor\");\n\n        if (!this.running()) return;\n\n        if (this.cancelIfAppropriate()) return;\n        if (this.finishIfAppropriate()) return;\n\n        int tableIndex = processTick - 1;\n\n        if (this.eventTable.containsKey(tableIndex)) this.eventTable.get(tableIndex).accept(this, this.target);\n        if (this.processStepTable.containsKey(tableIndex)) this.activeSteps.add(this.processStepTable.get(tableIndex).createInfo(tableIndex));\n\n        this.activeSteps.removeIf(stepInfo -> !stepInfo.tick(this));\n\n        this.processTick++;\n    }\n\n    /**\n     * Attempts to begin execution\n     *\n     * @return {@code true} if execution will start next tick,\n     * {@code false} if execution is already running\n     */\n    public boolean begin() {\n        if (this.processTick != 0) return false;\n\n        this.processTick = 1;\n        return true;\n    }\n\n    /**\n     * @return {@code true} if this executor is currently running\n     */\n    @SuppressWarnings(\"BooleanMethodIsAlwaysInverted\")\n    public boolean running() {\n        return this.processTick > 0;\n    }\n\n    /**\n     * @return The last processing tick this executor completed\n     */\n    public int getProcessTick() {\n        return processTick;\n    }\n\n    /**\n     * @return The object this executor is operating on\n     */\n    public T getTarget() {\n        return target;\n    }\n\n    /**\n     * Attempts to instantly cancel execution\n     *\n     * @return {@code true} if execution was successfully cancelled,\n     * {@code false} if this executor was not running\n     */\n    public boolean cancel() {\n        if (!this.running()) return false;\n\n        this.processTick = 0;\n        this.activeSteps.clear();\n\n        if (this.eventTable.containsKey(CANCEL_EVENT_INDEX)) this.eventTable.get(CANCEL_EVENT_INDEX).accept(this, this.target);\n\n        return true;\n    }\n\n    private boolean finishIfAppropriate() {\n        if (!this.running()) return false;\n        if (this.processTick < processLength) return false;\n\n        if (this.eventTable.containsKey(FINISH_EVENT_INDEX)) this.eventTable.get(FINISH_EVENT_INDEX).accept(this, this.target);\n\n        this.processTick = 0;\n        this.activeSteps.clear();\n        return true;\n    }\n\n    private boolean cancelIfAppropriate() {\n        if (this.condition.test(this)) return false;\n        this.cancel();\n        return true;\n    }\n\n    /**\n     * Saves the state of this executor\n     *\n     * @param targetTag The nbt to write state into\n     */\n    public void writeState(CompoundTag targetTag) {\n        targetTag.putInt(\"ProcessTick\", processTick);\n    }\n\n    /**\n     * Restores the saved state of this executor\n     *\n     * @param targetTag The nbt to read state from\n     */\n    public void readState(CompoundTag targetTag) {\n        this.processTick = targetTag.getIntOr(\"ProcessTick\", 0);\n\n        activeSteps.clear();\n        processStepTable.forEach((index, step) -> {\n            if (processTick >= index && processTick <= index + step.length) {\n                activeSteps.add(step.createInfo(index, processTick - index));\n            }\n        });\n    }\n\n    @ApiStatus.Internal\n    public record ProcessStep<T>(int length, BiConsumer<LinearProcessExecutor<T>, T> executor) {\n\n        public Info<T> createInfo(int index) {\n            return new Info<>(index, this);\n        }\n\n        public Info<T> createInfo(int index, int tick) {\n            return new Info<>(index, tick, this);\n        }\n\n        public static final class Info<T> {\n\n            private final ProcessStep<T> step;\n            private final int index;\n\n            private int tick = 0;\n\n            public Info(int index, ProcessStep<T> step) {\n                this.index = index;\n                this.step = step;\n            }\n\n            public Info(int index, int tick, ProcessStep<T> step) {\n                this.index = index;\n                this.tick = tick;\n                this.step = step;\n            }\n\n            public boolean tick(LinearProcessExecutor<T> target) {\n                this.tick++;\n                if (this.tick == step.length) return false;\n\n                this.step.executor.accept(target, target.getTarget());\n\n                return true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/AlignmentLerp.java",
    "content": "package io.wispforest.owo.braid.animation;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport net.minecraft.util.Mth;\n\npublic class AlignmentLerp extends Lerp<Alignment> {\n\n    public AlignmentLerp(Alignment start, Alignment end) {\n        super(start, end);\n    }\n\n    @Override\n    protected Alignment at(double t) {\n        return Alignment.of(\n            Mth.lerp(t, this.start.horizontal(), this.end.horizontal()),\n            Mth.lerp(t, this.start.vertical(), this.end.vertical())\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/Animation.java",
    "content": "package io.wispforest.owo.braid.animation;\n\nimport io.wispforest.owo.braid.framework.proxy.ProxyHost;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\n\npublic class Animation {\n\n    private final Scheduler scheduler;\n    private final Listener listener;\n    private final @Nullable FinishListener finishListener;\n\n    public Easing easing;\n    public Duration duration;\n\n    private double progress;\n    private @Nullable Target target;\n\n    public Animation(Easing easing, Duration duration, Scheduler scheduler, Listener listener, @Nullable FinishListener finishListener, Target startFrom) {\n        this.easing = easing;\n        this.duration = duration;\n        this.scheduler = scheduler;\n        this.listener = listener;\n        this.finishListener = finishListener;\n        this.progress = startFrom.targetProgress;\n    }\n\n    public Animation(Easing easing, Duration duration, Scheduler scheduler, Listener listener, Target startFrom) {\n        this(easing, duration, scheduler, listener, null, startFrom);\n    }\n\n    public @Nullable Target target() {\n        return this.target;\n    }\n\n    public double progress() {\n        return this.easing.apply((float) this.progress);\n    }\n\n    public void towards(Target target) {\n        this.towards(target, true);\n    }\n\n    public void towards(Target target, boolean restart) {\n        if (restart) {\n            this.progress = 1 - target.targetProgress;\n        }\n\n        if (this.target == null) {\n            this.scheduler.schedule(this::callback);\n        }\n\n        this.target = target;\n    }\n\n    public void pause() {\n        this.target = null;\n    }\n\n    public void stop() {\n        this.stop(null);\n    }\n\n    public void stop(@Nullable Target at) {\n        if (this.target == null && at == null) return;\n\n        this.progress = at != null ? at.targetProgress : this.target.targetProgress;\n        this.target = null;\n    }\n\n    private void callback(Duration delta) {\n        if (this.target == null) return;\n\n        this.progress = Mth.clamp(\n            this.progress + this.target.direction * delta.toNanos() / (double) this.duration.toNanos(),\n            0,\n            1\n        );\n\n        this.listener.onUpdate(this.easing.apply((float) this.progress));\n\n        if (Math.abs(this.progress - this.target.targetProgress) > EPSILON) {\n            this.scheduler.schedule(this::callback);\n        } else {\n            if (this.finishListener != null) {\n                this.finishListener.onFinished(this.target);\n            }\n\n            this.progress = this.target.targetProgress;\n            this.target = null;\n        }\n    }\n\n    // ---\n\n    private static final double EPSILON = 1e-3;\n\n    // ---\n\n    public enum Target {\n        START(-1, 0),\n        END(1, 1);\n\n        public final long direction;\n        public final double targetProgress;\n\n        Target(long direction, double targetProgress) {\n            this.direction = direction;\n            this.targetProgress = targetProgress;\n        }\n    }\n\n    @FunctionalInterface\n    public interface Listener {\n        void onUpdate(double progress);\n    }\n\n    @FunctionalInterface\n    public interface FinishListener {\n        void onFinished(Target atTarget);\n    }\n\n    @FunctionalInterface\n    public interface Scheduler {\n        void schedule(ProxyHost.AnimationCallback callback);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/AutomaticallyAnimatedWidget.java",
    "content": "package io.wispforest.owo.braid.animation;\n\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport org.apache.commons.lang3.mutable.MutableBoolean;\nimport org.jetbrains.annotations.Nullable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.time.Duration;\nimport java.util.Objects;\n\npublic abstract class AutomaticallyAnimatedWidget extends StatefulWidget {\n\n    private static final Logger log = LoggerFactory.getLogger(AutomaticallyAnimatedWidget.class);\n    public final Duration duration;\n    public final Easing easing;\n\n    protected AutomaticallyAnimatedWidget(Duration duration, Easing easing) {\n        this.duration = duration;\n        this.easing = easing;\n    }\n\n    @Override\n    public abstract State<?> createState();\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public static abstract class State<T extends AutomaticallyAnimatedWidget> extends WidgetState<T> {\n\n        private Animation animation;\n        private LerpVisitor activeVisitor;\n\n        private void callback(double progress) {\n            this.setState(() -> {});\n        }\n\n        @Override\n        public void init() {\n            this.animation = new Animation(\n                this.widget().easing,\n                this.widget().duration,\n                this::scheduleAnimationCallback,\n                this::callback,\n                Animation.Target.END\n            );\n\n            this.visitLerps((previous, targetValue, factory) -> {\n                return factory.make(targetValue, targetValue);\n            });\n        }\n\n        @Override\n        public void didUpdateWidget(AutomaticallyAnimatedWidget oldWidget) {\n            var restartAnimation = new MutableBoolean(this.widget().easing != oldWidget.easing);\n            this.animation.duration = this.widget().duration;\n\n            if (restartAnimation.isFalse()) {\n                this.visitLerps((previous, targetValue, factory) -> {\n                    if (!Objects.equals(previous.end, targetValue)) {\n                        restartAnimation.setTrue();\n                    }\n\n                    return previous;\n                });\n            }\n\n            if (restartAnimation.isTrue()) {\n                this.visitLerps((previous, targetValue, factory) -> factory.make(previous.compute(this.animationValue()), targetValue));\n                this.animation.easing = this.widget().easing;\n                this.animation.towards(Animation.Target.END);\n            }\n        }\n\n        private void visitLerps(LerpVisitor visitor) {\n            this.activeVisitor = visitor;\n            this.updateLerps();\n        }\n\n        // ---\n\n        protected double animationValue() {\n            return this.animation.progress();\n        }\n\n        protected <L extends Lerp<V>, V> L visitLerp(@Nullable Lerp<V> previous, V targetValue, Lerp.Factory<L, V> factory) {\n            return (L) this.activeVisitor.visit(previous, targetValue, factory);\n        }\n\n        protected <L extends Lerp<V>, V> L visitNullableLerp(@Nullable Lerp<V> previous, V targetValue, Lerp.Factory<L, V> factory) {\n            return (L) this.activeVisitor.visit(previous, targetValue, (start, end) -> new NullableLerp(start, end, factory));\n        }\n\n        protected abstract void updateLerps();\n    }\n\n    @FunctionalInterface\n    private interface LerpVisitor<L extends Lerp<V>, V> {\n        L visit(@Nullable Lerp<V> previous, V targetValue, Lerp.Factory<L, V> factory);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/ColorLerp.java",
    "content": "package io.wispforest.owo.braid.animation;\n\nimport io.wispforest.owo.braid.core.Color;\n\npublic class ColorLerp extends Lerp<Color> {\n\n    public ColorLerp(Color start, Color end) {\n        super(start, end);\n    }\n\n    @Override\n    protected Color at(double t) {\n        return Color.mix(t, this.start, this.end);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/DoubleLerp.java",
    "content": "package io.wispforest.owo.braid.animation;\n\nimport net.minecraft.util.Mth;\n\npublic class DoubleLerp extends Lerp<Double> {\n\n    public DoubleLerp(Double start, Double end) {\n        super(start, end);\n    }\n\n    @Override\n    protected Double at(double t) {\n        return Mth.lerp(t, this.start, this.end);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/Easing.java",
    "content": "package io.wispforest.owo.braid.animation;\n\npublic class Easing {\n\n    public static final Easing LINEAR = new Easing(x -> x);\n    public static final Easing IN_QUAD = new Easing(x -> x * x);\n    public static final Easing OUT_QUAD = new Easing(x -> 1.0 - (1.0 - x) * (1.0 - x));\n    public static final Easing IN_OUT_QUAD = new Easing(x -> x < 0.5 ? 2.0 * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 2.0) / 2.0);\n    public static final Easing IN_CUBIC = new Easing(x -> x * x * x);\n    public static final Easing OUT_CUBIC = new Easing(x -> 1.0 - Math.pow(1.0 - x, 3));\n    public static final Easing IN_OUT_CUBIC = new Easing(x -> x < 0.5 ? 4.0 * x * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 3.0) / 2.0);\n    public static final Easing IN_QUART = new Easing(x -> x * x * x * x);\n    public static final Easing OUT_QUART = new Easing(x -> 1.0 - Math.pow(1.0 - x, 4.0));\n    public static final Easing IN_OUT_QUART = new Easing(x -> x < 0.5 ? 8.0 * x * x * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 4.0) / 2.0);\n    public static final Easing IN_QUINT = new Easing(x -> x * x * x * x * x);\n    public static final Easing OUT_QUINT = new Easing(x -> 1.0 - Math.pow(1.0 - x, 5.0));\n    public static final Easing IN_OUT_QUINT = new Easing(x -> x < 0.5 ? 16.0 * x * x * x * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 5.0) / 2.0);\n    public static final Easing IN_SINE = new Easing(x -> 1.0 - Math.cos((x * Math.PI) / 2.0));\n    public static final Easing OUT_SINE = new Easing(x -> Math.sin((x * Math.PI) / 2.0));\n    public static final Easing IN_OUT_SINE = new Easing(x -> -(Math.cos(Math.PI * x) - 1) / 2.0);\n    public static final Easing IN_EXPO = new Easing(x -> x == 0.0 ? 0.0 : Math.pow(2.0, 10.0 * x - 10.0));\n    public static final Easing OUT_EXPO = new Easing(x -> x == 1.0 ? 1.0 : 1.0 - Math.pow(2.0, -10.0 * x));\n    public static final Easing IN_OUT_EXPO = new Easing(x -> x == 0.0 ? 0.0 : x == 1.0 ? 1.0 : x < 0.5 ? Math.pow(2.0, 20.0 * x - 10.0) / 2.0 : (2.0 - Math.pow(2.0, -20.0 * x + 10.0)) / 2.0);\n    public static final Easing IN_CIRC = new Easing(x -> 1.0 - Math.sqrt(1.0 - Math.pow(x, 2.0)));\n    public static final Easing OUT_CIRC = new Easing(x -> Math.sqrt(1.0 - Math.pow(x - 1.0, 2.0)));\n    public static final Easing IN_OUT_CIRC = new Easing(x -> x < 0.5 ? (1.0 - Math.sqrt(1.0 - Math.pow(2.0 * x, 2.0))) / 2 : (Math.sqrt(1.0 - Math.pow(-2.0 * x + 2.0, 2.0)) + 1.0) / 2.0);\n\n    public static final Easing OUT_BOUNCE = new Easing(x -> {\n        var n1 = 7.5625;\n        var d1 = 2.75;\n\n        if (x < 1 / d1) {\n            return n1 * x * x;\n        } else if (x < 2 / d1) {\n            return n1 * (x -= 1.5 / d1) * x + 0.75;\n        } else if (x < 2.5 / d1) {\n            return n1 * (x -= 2.25 / d1) * x + 0.9375;\n        } else {\n            return n1 * (x -= 2.625 / d1) * x + 0.984375;\n        }\n    });\n\n    // ---\n\n    private final Function function;\n    public Easing(Function function) {\n        this.function = function;\n    }\n\n    public final double apply(double x) {\n        if (x == 0 || x == 1) return x;\n        return this.compute(x);\n    }\n\n    protected double compute(double x) {\n        return this.function.compute(x);\n    }\n\n    @FunctionalInterface\n    public interface Function {\n        double compute(double x);\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/InsetsLerp.java",
    "content": "package io.wispforest.owo.braid.animation;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport net.minecraft.util.Mth;\n\npublic class InsetsLerp extends Lerp<Insets> {\n\n    public InsetsLerp(Insets start, Insets end) {\n        super(start, end);\n    }\n\n    @Override\n    protected Insets at(double t) {\n        return Insets.of(\n            Mth.lerp(t, this.start.top(), this.end.top()),\n            Mth.lerp(t, this.start.bottom(), this.end.bottom()),\n            Mth.lerp(t, this.start.left(), this.end.left()),\n            Mth.lerp(t, this.start.right(), this.end.right())\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/Lerp.java",
    "content": "package io.wispforest.owo.braid.animation;\n\npublic abstract class Lerp<T> {\n\n    public final T start;\n    public final T end;\n\n    protected Lerp(T start, T end) {\n        this.start = start;\n        this.end = end;\n    }\n\n    public T compute(double t) {\n        if (t - EPSILON <= 0) return this.start;\n        if (t + EPSILON >= 1) return this.end;\n\n        return this.at(t);\n    }\n\n    protected abstract T at(double t);\n\n    // ---\n\n    private static final double EPSILON = 1e-4;\n\n    // ---\n\n    @FunctionalInterface\n    public interface Factory<T extends Lerp<V>, V> {\n        T make(V start, V end);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/animation/NullableLerp.java",
    "content": "package io.wispforest.owo.braid.animation;\n\nimport org.jetbrains.annotations.Nullable;\n\npublic class NullableLerp<T> extends Lerp<T> {\n\n    private final @Nullable Lerp<T> delegate;\n\n    public NullableLerp(@Nullable T start, @Nullable T end, Lerp.Factory<Lerp<T>, T> delegateFactory) {\n        super(start, end);\n        if (start != null) {\n            this.delegate = delegateFactory.make(start, end);\n        } else {\n            this.delegate = null;\n        }\n    }\n\n    @Override\n    protected T at(double t) {\n        return this.delegate != null ? this.delegate.at(t) : this.end;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Aabb2d.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2f;\n\npublic class Aabb2d {\n\n    public double x;\n    public double y;\n    public double width;\n    public double height;\n\n    public Aabb2d(double x, double y, double width, double height) {\n        this.x = x;\n        this.y = y;\n        this.width = width;\n        this.height = height;\n    }\n\n    public double minX() {\n        return this.x;\n    }\n\n    public double maxX() {\n        return this.x + this.width;\n    }\n\n    public double minY() {\n        return this.y;\n    }\n\n    public double maxY() {\n        return this.y + this.height;\n    }\n\n    public Aabb2d transform(Matrix3x2f matrix) {\n        var topLeft = matrix.transformPosition((float) this.x, (float) this.y, new Vector2f());\n        var topRight = matrix.transformPosition((float) (this.x + this.width), (float) this.y, new Vector2f());\n        var bottomLeft = matrix.transformPosition((float) this.x, (float) (this.y + this.height), new Vector2f());\n        var bottomRight = matrix.transformPosition((float) (this.x + this.width), (float) (this.y + this.height), new Vector2f());\n\n        this.x = Math.min(Math.min(Math.min(topLeft.x, topRight.x), bottomLeft.x), bottomRight.x);\n        this.width = Math.max(Math.max(Math.max(topLeft.x, topRight.x), bottomLeft.x), bottomRight.x) - this.x;\n\n        this.y = Math.min(Math.min(Math.min(topLeft.y, topRight.y), bottomLeft.y), bottomRight.y);\n        this.height = Math.max(Math.max(Math.max(topLeft.y, topRight.y), bottomLeft.y), bottomRight.y) - this.y;\n\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Alignment.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport org.jetbrains.annotations.ApiStatus;\n\npublic record Alignment(double horizontal, double vertical) {\n    public static final Alignment TOP_LEFT = Alignment.of(0, 0);\n    public static final Alignment TOP = Alignment.of(.5, 0);\n    public static final Alignment TOP_RIGHT = Alignment.of(1, 0);\n    public static final Alignment LEFT = Alignment.of(0, .5);\n    public static final Alignment CENTER = Alignment.of(.5, .5);\n    public static final Alignment RIGHT = Alignment.of(1, .5);\n    public static final Alignment BOTTOM_LEFT = Alignment.of(0, 1);\n    public static final Alignment BOTTOM = Alignment.of(.5, 1);\n    public static final Alignment BOTTOM_RIGHT = Alignment.of(1, 1);\n\n    // ---\n\n    @ApiStatus.Internal\n    @Deprecated(forRemoval = true)\n    public Alignment {}\n\n    public static Alignment of(double horizontal, double vertical) {\n        return new Alignment(horizontal, vertical);\n    }\n\n    public double alignHorizontal(double space, double object) {\n        return Math.floor((space - object) * horizontal);\n    }\n\n    public double alignVertical(double space, double object) {\n        return Math.floor((space - object) * vertical);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/AppState.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.google.common.collect.Iterables;\nimport com.mojang.blaze3d.opengl.GlStateManager;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.core.events.*;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.*;\nimport io.wispforest.owo.braid.framework.proxy.BuildScope;\nimport io.wispforest.owo.braid.framework.proxy.ProxyHost;\nimport io.wispforest.owo.braid.framework.proxy.SingleChildInstanceWidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Tooltip;\nimport io.wispforest.owo.braid.widgets.basic.VisitorWidget;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventStream;\nimport io.wispforest.owo.braid.widgets.focus.FocusClickArea;\nimport io.wispforest.owo.braid.widgets.focus.RootFocusScope;\nimport io.wispforest.owo.braid.widgets.inspector.BraidInspector;\nimport io.wispforest.owo.braid.widgets.inspector.InstancePicker;\nimport io.wispforest.owo.util.EventSource;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.network.chat.Style;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\nimport org.joml.Vector2dc;\nimport org.joml.Vector2f;\nimport org.lwjgl.glfw.GLFW;\nimport org.slf4j.Logger;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\nimport java.util.function.Consumer;\n\npublic class AppState implements InstanceHost, ProxyHost {\n\n    public final @Nullable Logger logger;\n    private final Minecraft client;\n\n    public final Surface surface;\n    public final EventBinding eventBinding;\n\n    private final BuildScope rootBuildScope = new BuildScope();\n    private Deque<AnimationCallback> animationCallbacks = new LinkedList<>();\n    private final PriorityQueue<ScheduledCallback> callbacks = new PriorityQueue<>();\n    private Deque<Runnable> postLayoutCallbacks = new LinkedList<>();\n    private final String name;\n    private final RootProxy root;\n\n    private final Vector2d cursorPosition = new Vector2d();\n\n    private Set<MouseListener> hovered = new HashSet<>();\n    private final WeakHashMap<MouseListener, MousePosition> mousePositions = new WeakHashMap<>();\n    private @Nullable MouseListener dragging = null;\n    private @Nullable CursorStyle draggingCursorStyle = null;\n    private int draggingButton = -1;\n    private KeyModifiers draggingModifiers = null;\n    private boolean dragStarted = false;\n\n    private static final Duration MIN_GRACE_PERIOD = Duration.ofMillis(200);\n    private static final Duration MAX_GRACE_PERIOD = Duration.ofMillis(500);\n    private static final int SCROLL_MOVEMENT_THRESHOLD = 5;\n\n    private @Nullable HitTestState scrollHit = null;\n    private Vector2d scrollPos = new Vector2d();\n    private Instant lastScrollTime = Instant.EPOCH;\n\n    private final BraidEventStream<RootFocusScope.KeyDownEvent> keyDownStream = new BraidEventStream<>();\n    private final BraidEventStream<RootFocusScope.KeyUpEvent> keyUpStream = new BraidEventStream<>();\n    private final BraidEventStream<RootFocusScope.CharEvent> charStream = new BraidEventStream<>();\n\n    private final BraidHotReloadCallback.Listener reloadListener;\n    private final EventSource<?>.Subscription resizeSubscription;\n    private final List<Runnable> onTerminate = new ArrayList<>();\n    private boolean running = true;\n\n    private final BraidInspector inspector = new BraidInspector(this);\n\n    public AppState(\n        @Nullable Logger logger,\n        @Nullable String name,\n        Minecraft client,\n        Surface surface,\n        EventBinding eventBinding,\n        Widget root\n    ) {\n        this.logger = logger;\n        this.client = client;\n\n        this.surface = surface;\n        this.eventBinding = eventBinding;\n\n        this.name = name != null ? name : root.getClass().getName();\n        this.root = new RootWidget(\n            new AppWidget(\n                this,\n                new InstancePicker(\n                    this.inspector.onPick(),\n                    this.inspector::revealInstance,\n                    new RootFocusScope(\n                        this.keyDownStream.source(),\n                        this.keyUpStream.source(),\n                        this.charStream.source(),\n                        new UserRoot(\n                            widgetProxy -> inspector.rootProxy = widgetProxy,\n                            widgetInstance -> inspector.rootInstance = widgetInstance,\n                            root\n                        )\n                    )\n                )\n            ),\n            this.rootBuildScope\n        ).proxy();\n        this.root.bootstrap(this, this);\n        this.scheduleLayout(this.rootInstance());\n\n        this.reloadListener = BraidHotReloadCallback.register();\n        this.resizeSubscription = this.surface.onResize().subscribe((newWidth, newHeight) -> {\n            this.rootInstance().markNeedsLayout();\n        });\n    }\n\n    public boolean running() {\n        return this.running;\n    }\n\n    public void onTerminate(Runnable callback) {\n        this.onTerminate.add(callback);\n    }\n\n    public void scheduleShutdown() {\n        this.running = false;\n        this.onTerminate.forEach(Runnable::run);\n    }\n\n    public void activateInspector() {\n        this.inspector.activate();\n    }\n\n    private @Nullable TooltipState activeTooltip;\n\n    public void draw(GuiGraphics graphics) {\n        this.surface.beginRendering();\n\n        graphics.push();\n        this.rootInstance().transform.transformToParent(graphics.pose());\n\n        var braidContext = BraidGraphics.create(graphics, this.surface);\n\n        GlStateManager._enableScissorTest();\n        this.rootInstance().draw(braidContext);\n        GlStateManager._disableScissorTest();\n\n        if (this.activeTooltip != null) {\n            if (this.activeTooltip.components() != null) braidContext.drawTooltip(this.client.font, this.activeTooltip.x(), this.activeTooltip.y(), this.activeTooltip.components());\n            if (this.activeTooltip.style() != null) graphics.renderComponentHoverEffect(this.client.font, this.activeTooltip.style(), this.activeTooltip.x(), this.activeTooltip.y());\n        }\n\n        graphics.pop();\n\n        this.surface.endRendering();\n    }\n\n    public void processEvents(float frameDeltaInTicks) {\n        this.pollAndDispatchEvents();\n\n        var state = this.hitTest();\n\n        var tooltipSupplier = state.firstWhere(hit -> hit.instance() instanceof TooltipProvider);\n        if (tooltipSupplier != null) {\n            var tooltip = (TooltipProvider) tooltipSupplier.instance();\n            var components = tooltip.getTooltipComponentsAt(tooltipSupplier.x(), tooltipSupplier.y());\n            var style = tooltip.getStyleAt(tooltipSupplier.x(), tooltipSupplier.y());\n\n            if (components != null || style != null) this.activeTooltip = new TooltipState(components, style, (int) this.cursorPosition.x, (int) this.cursorPosition.y);\n        } else {\n            this.activeTooltip = null;\n        }\n\n        // ---\n\n        var nowHovered = new HashSet<MouseListener>();\n        for (var hit : Iterables.filter(state.occludedTrace(), hit -> hit.instance() instanceof MouseListener)) {\n            var listener = (MouseListener) hit.instance();\n\n            nowHovered.add(listener);\n\n            if (this.hovered.contains(listener)) {\n                this.hovered.remove(listener);\n            } else {\n                listener.onMouseEnter();\n            }\n\n            var mousePosition = this.mousePositions.getOrDefault(listener, MousePosition.ORIGIN);\n            if (mousePosition.x() != hit.x() || mousePosition.y() != hit.y()) {\n                listener.onMouseMove(hit.x(), hit.y());\n                this.mousePositions.put(listener, new MousePosition(hit.x(), hit.y()));\n            }\n        }\n\n        for (var noLongerHovered : this.hovered) {\n            noLongerHovered.onMouseExit();\n        }\n\n        this.hovered = nowHovered;\n\n        // ---\n\n        @Nullable CursorStyle activeStyle = null;\n        if (this.dragging != null) {\n            activeStyle = this.draggingCursorStyle;\n        } else {\n            var cursorStyleSource = state.firstWhere(\n                (hit) ->\n                    hit.instance() instanceof MouseListener &&\n                        ((MouseListener) hit.instance()).cursorStyleAt(hit.x(), hit.y()) != null\n            );\n\n            if (cursorStyleSource != null) {\n                activeStyle = ((MouseListener) cursorStyleSource.instance()).cursorStyleAt(\n                    cursorStyleSource.x(),\n                    cursorStyleSource.y()\n                );\n            }\n        }\n\n        this.surface.setCursorStyle(activeStyle != null ? activeStyle : CursorStyle.NONE);\n\n        // ---\n\n        if (this.reloadListener.poll()) {\n            this.rebuildRoot();\n        }\n\n        if (!this.animationCallbacks.isEmpty()) {\n            var callbacksForThisFrame = this.animationCallbacks;\n            this.animationCallbacks = new LinkedList<>();\n\n            while (!callbacksForThisFrame.isEmpty()) {\n                callbacksForThisFrame.poll().run(Duration.ofMillis((long) (frameDeltaInTicks * 50)));\n            }\n        }\n\n        var now = Instant.now();\n        while (!this.callbacks.isEmpty() && this.callbacks.peek().after().isBefore(now)) {\n            this.callbacks.poll().callback().run();\n        }\n\n        var anyTreeMutations = false;\n\n        anyTreeMutations |= this.rootBuildScope.rebuildDirtyProxies();\n        anyTreeMutations |= this.flushLayoutQueue();\n\n        if (anyTreeMutations) {\n            this.inspector.refresh();\n        }\n\n        if (!this.postLayoutCallbacks.isEmpty()) {\n            var callbacksForThisFrame = this.postLayoutCallbacks;\n            this.postLayoutCallbacks = new LinkedList<>();\n\n            while (!callbacksForThisFrame.isEmpty()) {\n                callbacksForThisFrame.poll().run();\n            }\n        }\n    }\n\n    private void pollAndDispatchEvents() {\n        var events = this.eventBinding.poll();\n\n        for (var slot : events) {\n            switch (slot.event) {\n                case MouseButtonPressEvent(int button, KeyModifiers modifiers) -> {\n                    this.scrollHit = null;\n                    var state = this.hitTest();\n\n                    state.firstWhere(hit -> {\n                        if (!(hit.instance() instanceof FocusClickArea.Instance instance)) return false;\n\n                        instance.widget().clickCallback.run();\n                        return true;\n                    });\n\n                    var clicked = state.firstWhere(\n                        (hit) -> hit.instance() instanceof MouseListener && ((MouseListener) hit.instance()).onMouseDown(hit.x(), hit.y(), button, modifiers)\n                    );\n\n                    if (clicked != null) {\n                        slot.markHandled();\n\n                        if (this.dragging == null) {\n                            this.dragging = (MouseListener) clicked.instance();\n                            this.draggingCursorStyle = ((MouseListener) clicked.instance()).cursorStyleAt(\n                                clicked.x(),\n                                clicked.y()\n                            );\n                            this.dragStarted = false;\n                            this.draggingButton = button;\n                            this.draggingModifiers = modifiers;\n                        }\n                    }\n                }\n                case MouseMoveEvent(double x, double y) -> {\n                    slot.markHandled();\n\n                    var deltaX = x - this.cursorPosition.x;\n                    var deltaY = y - this.cursorPosition.y;\n                    if (deltaX == 0 && deltaY == 0) break;\n\n                    this.cursorPosition.x = x;\n                    this.cursorPosition.y = y;\n                    if (this.cursorPosition.distance(this.scrollPos) > SCROLL_MOVEMENT_THRESHOLD) this.scrollHit = null;\n\n                    if (!(this.dragging instanceof WidgetInstance<?>)) break;\n\n                    if (!this.dragStarted) {\n                        this.dragging.onMouseDragStart(draggingButton, draggingModifiers);\n                        this.dragStarted = true;\n                    }\n\n                    var globalTransform = ((WidgetInstance<?>) this.dragging).computeGlobalTransform();\n                    var coordinates = new Vector2f((float) x, (float) y);\n                    globalTransform.transformPosition(coordinates);\n\n                    // apply *only the rotation* of the instance's transform\n                    // to the mouse movement\n                    var delta = new Vector2f((float) deltaX, (float) deltaY);\n                    globalTransform.transformDirection(delta);\n\n                    this.dragging.onMouseDrag(coordinates.x, coordinates.y, delta.x, delta.y);\n                }\n                case MouseButtonReleaseEvent(int button, KeyModifiers modifiers) -> {\n                    this.scrollHit = null;\n                    var state = this.hitTest();\n                    var unClicked = state.firstWhere(\n                        (hit) -> hit.instance() instanceof MouseListener && ((MouseListener) hit.instance()).onMouseUp(hit.x(), hit.y(), button, modifiers)\n                    );\n\n                    if (unClicked != null) {\n                        slot.markHandled();\n                    }\n\n                    if (this.draggingButton == button) {\n                        if (this.dragStarted && this.dragging != null) {\n                            this.dragging.onMouseDragEnd();\n                        }\n\n                        this.dragging = null;\n                    }\n                }\n                case MouseScrollEvent(double xOffset, double yOffset) -> {\n                    var now = Instant.now();\n                    var grace = this.cursorPosition.distance(this.scrollPos) > SCROLL_MOVEMENT_THRESHOLD ? MIN_GRACE_PERIOD : MAX_GRACE_PERIOD;\n                    if (this.scrollHit == null || now.minus(grace).isAfter(this.lastScrollTime) ) this.scrollHit = this.hitTest();\n                    this.lastScrollTime = now;\n                    this.scrollPos = new Vector2d(this.cursorPosition);\n                    var scrolled = this.scrollHit.firstWhere(\n                        (hit) -> hit.instance() instanceof MouseListener &&\n                            ((MouseListener) hit.instance()).onMouseScroll(\n                                hit.x(),\n                                hit.y(),\n                                xOffset,\n                                yOffset\n                            )\n                    );\n\n                    if (scrolled != null) {\n                        slot.markHandled();\n                    }\n                }\n                case KeyPressEvent(int keyCode, int scancode, KeyModifiers modifiers) -> {\n                    if (keyCode == GLFW.GLFW_KEY_R && modifiers.shift() && modifiers.alt()) {\n                        this.rebuildRoot();\n                        slot.markHandled();\n\n                        break;\n                    }\n\n                    if (keyCode == GLFW.GLFW_KEY_I && modifiers.ctrl() && modifiers.shift()) {\n                        this.inspector.activate();\n                        slot.markHandled();\n\n                        break;\n                    }\n\n                    var event = new RootFocusScope.KeyDownEvent(keyCode, modifiers);\n                    this.keyDownStream.sink().onEvent(event);\n\n                    if (event.handled()) {\n                        slot.markHandled();\n                    }\n                }\n                case KeyReleaseEvent(int keycode, int scancode, KeyModifiers modifiers) -> {\n                    var event = new RootFocusScope.KeyUpEvent(keycode, modifiers);\n                    this.keyUpStream.sink().onEvent(event);\n\n                    if (event.handled()) {\n                        slot.markHandled();\n                    }\n                }\n                case CharInputEvent(char codepoint, KeyModifiers modifiers) -> {\n                    var event = new RootFocusScope.CharEvent(codepoint, modifiers);\n                    this.charStream.sink().onEvent(event);\n\n                    if (event.handled()) {\n                        slot.markHandled();\n                    }\n                }\n                case FilesDroppedEvent filesDroppedEvent -> {}\n                case CloseEvent ignored -> {\n                    slot.markHandled();\n                    this.scheduleShutdown();\n                }\n            }\n        }\n    }\n\n    public void rebuildRoot() {\n        var before = Instant.now();\n\n        this.root.reassemble();\n\n        var elapsed = ChronoUnit.MICROS.between(before, Instant.now());\n        if (this.logger != null) this.logger.debug(\"completed full app rebuild in {}us\", elapsed);\n    }\n\n    public void dispose() {\n        this.inspector.close();\n\n        this.reloadListener.unregister();\n        this.resizeSubscription.cancel();\n\n        this.surface.dispose();\n        this.root.unmount();\n    }\n\n    private HitTestState hitTest() {\n        return this.hitTest(this.cursorPosition.x, this.cursorPosition.y);\n    }\n\n    public HitTestState hitTest(double x, double y) {\n        var state = new HitTestState();\n        this.rootInstance().hitTest(x, y, state);\n\n        return state;\n    }\n\n    // ---\n\n    @Override\n    public Minecraft client() {\n        return this.client;\n    }\n\n    public SingleChildWidgetInstance<?> rootInstance() {\n        return this.root.instance();\n    }\n\n    // ---\n\n    private List<WidgetInstance<?>> layoutQueue = new ArrayList<>();\n    private boolean mergeToLayoutQueue = false;\n\n    private boolean flushLayoutQueue() {\n        if (this.layoutQueue.isEmpty()) return false;\n\n        while (!this.layoutQueue.isEmpty()) {\n            var queue = this.layoutQueue;\n            this.layoutQueue = new ArrayList<>();\n\n            queue.sort(Comparator.naturalOrder());\n            for (var idx = 0; idx < queue.size(); idx++) {\n                var instance = queue.get(idx);\n\n                if (this.mergeToLayoutQueue) {\n                    this.mergeToLayoutQueue = false;\n\n                    if (!this.layoutQueue.isEmpty()) {\n                        this.layoutQueue.addAll(queue.subList(idx, queue.size()));\n                        break;\n                    }\n                }\n\n                if (instance.needsLayout()) {\n                    instance.layout(\n                        instance.hasParent()\n                            ? instance.constraints()\n                            : Constraints.tight(Size.of(this.surface.width(), this.surface.height()))\n                    );\n                }\n            }\n\n            this.mergeToLayoutQueue = false;\n        }\n\n        return true;\n    }\n\n    @Override\n    public void scheduleLayout(WidgetInstance<?> instance) {\n        this.layoutQueue.add(instance);\n    }\n\n    @Override\n    public void notifySubtreeRebuild() {\n        this.mergeToLayoutQueue = true;\n    }\n\n    @Override\n    public void scheduleAnimationCallback(AnimationCallback callback) {\n        this.animationCallbacks.offer(callback);\n    }\n\n    @Override\n    public long scheduleDelayedCallback(Duration delay, Runnable callback) {\n        var id = ScheduledCallback.nextId++;\n        this.callbacks.add(new ScheduledCallback(\n            Instant.now().plus(delay),\n            callback, id\n        ));\n        return id;\n    }\n\n    @Override\n    public void cancelDelayedCallback(long id) {\n        this.callbacks.removeIf(scheduledCallback -> scheduledCallback.id() == id);\n    }\n\n    @Override\n    public void schedulePostLayoutCallback(Runnable callback) {\n        this.postLayoutCallbacks.offer(callback);\n    }\n\n    @Override\n    public Vector2dc cursorPosition() {\n        return this.cursorPosition;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"%s (AppState@%s)\", this.name, Integer.toHexString(hashCode()));\n    }\n\n    // ---\n\n    public static String formatName(String category, Widget userRoot) {\n        var classPath = userRoot.getClass().getName().split(\"\\\\.\");\n        return String.format(\"%s[%s]\", category, classPath[classPath.length - 1]);\n    }\n\n    public static String formatName(String category, Widget userRoot, String... attributes) {\n        var classPath = userRoot.getClass().getName().split(\"\\\\.\");\n        return String.format(\"%s[%s, %s]\", category, String.join(\", \", attributes), classPath[classPath.length - 1]);\n    }\n\n    public static AppState of(BuildContext context) {\n        //noinspection DataFlowIssue\n        return context.getAncestor(AppWidget.class).app;\n    }\n}\n\nrecord ScheduledCallback(Instant after, Runnable callback, long id) implements Comparable<ScheduledCallback> {\n    //\"fuck you we starting at 7\" -chyz\n    public static long nextId = 7;\n\n    @Override\n    public int compareTo(@NotNull ScheduledCallback o) {\n        return this.after.compareTo(o.after);\n    }\n}\n\nclass RootWidget extends SingleChildInstanceWidget {\n\n    public final BuildScope rootBuildScope;\n\n    public RootWidget(Widget child, BuildScope rootBuildScope) {\n        super(child);\n        this.rootBuildScope = rootBuildScope;\n    }\n\n    @Override\n    public RootProxy proxy() {\n        return new RootProxy(this);\n    }\n\n    @Override\n    public RootInstance instantiate() {\n        return new RootInstance(this);\n    }\n}\n\nclass RootProxy extends SingleChildInstanceWidgetProxy {\n    public RootProxy(RootWidget widget) {\n        super(widget);\n    }\n\n    @Override\n    public BuildScope buildScope() {\n        return ((RootWidget) this.widget()).rootBuildScope;\n    }\n\n    @Override\n    public boolean mounted() {\n        return this.bootstrapped;\n    }\n\n    private boolean bootstrapped = false;\n\n    void bootstrap(InstanceHost instanceHost, ProxyHost proxyHost) {\n        this.bootstrapped = true;\n        this.lifecycle = Lifecycle.LIVE;\n\n        this.rootSetHost(proxyHost);\n\n        rebuild();\n        this.setDepth(0);\n\n        this.instance.setDepth(0);\n        this.instance.attachHost(instanceHost);\n    }\n}\n\nclass RootInstance extends SingleChildWidgetInstance.ShrinkWrap<RootWidget> {\n\n    public RootInstance(RootWidget widget) {\n        super(widget);\n    }\n}\n\nclass UserRoot extends VisitorWidget {\n\n    public final Consumer<WidgetProxy> proxyCallback;\n    public final Consumer<WidgetInstance<?>> instanceCallback;\n\n    public UserRoot(Consumer<WidgetProxy> proxyCallback, Consumer<WidgetInstance<?>> instanceCallback, Widget child) {\n        super(child);\n        this.proxyCallback = proxyCallback;\n        this.instanceCallback = instanceCallback;\n    }\n\n    private static final Visitor<UserRoot> VISITOR = (widget, instance) -> {\n        widget.instanceCallback.accept(instance);\n    };\n\n    @Override\n    public Proxy<?> proxy() {\n        var proxy = new Proxy<>(this, VISITOR);\n        this.proxyCallback.accept(proxy);\n\n        return proxy;\n    }\n}\n\nclass AppWidget extends InheritedWidget {\n\n    public final AppState app;\n\n    protected AppWidget(AppState app, Widget child) {\n        super(child);\n        this.app = app;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        if (((AppWidget) newWidget).app != this.app) {\n            throw new UnsupportedOperationException(\"changing the AppState of a widget tree is not supported\");\n        }\n\n        return false;\n    }\n}\n\nrecord TooltipState(@Nullable List<ClientTooltipComponent> components, @Nullable Style style, int x, int y) {}\n\nrecord MousePosition(double x, double y) {\n    public static final MousePosition ORIGIN = new MousePosition(0, 0);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/BraidGraphics.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport io.wispforest.owo.braid.core.element.BraidDashedLineElement;\nimport io.wispforest.owo.mixin.braid.Matrix3x2fStackAccessor;\nimport io.wispforest.owo.mixin.ui.access.GuiGraphicsAccessor;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.state.GuiRenderState;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix3x2fStack;\nimport org.joml.Matrix3x2fc;\n\nimport java.util.function.Consumer;\n\npublic class BraidGraphics extends OwoUIGraphics {\n\n    private final Surface surface;\n\n    protected BraidGraphics(Minecraft client, GuiRenderState renderState, int mouseX, int mouseY, Consumer<Runnable> setTooltipDrawer, Surface surface) {\n        super(client, renderState, mouseX, mouseY, setTooltipDrawer);\n        this.surface = surface;\n    }\n\n    public static BraidGraphics create(GuiGraphics grpahics, Surface surface) {\n        var braidContext = new BraidGraphics(\n            Minecraft.getInstance(),\n            grpahics.guiRenderState,\n            ((GuiGraphicsAccessor) grpahics).owo$getMouseX(),\n            ((GuiGraphicsAccessor) grpahics).owo$getMouseY(),\n            ((GuiGraphicsAccessor) grpahics)::owo$setDeferredTooltip,\n            surface\n        );\n        ((GuiGraphicsAccessor) braidContext).owo$setScissorStack(((GuiGraphicsAccessor) grpahics).owo$getScissorStack());\n        ((GuiGraphicsAccessor) braidContext).owo$setPose(new MatrixStack(((GuiGraphicsAccessor) grpahics).owo$getPose()));\n\n        return braidContext;\n    }\n\n    @Override\n    public int guiWidth() {\n        return this.surface.width();\n    }\n\n    @Override\n    public int guiHeight() {\n        return this.surface.height();\n    }\n\n    public void buildRectOutline(double x, double y, double width, double height, RectEdgeBuilder builder) {\n        builder.edge(x, y, x + width, y);\n        builder.edge(x, y + height, x + width, y + height);\n\n        builder.edge(x, y, x, y + height);\n        builder.edge(x + width, y, x + width, y + height);\n    }\n\n    public void drawDashedLine(RenderPipeline pipeline, double x1, double y1, double x2, double y2, double thiccness, double segmentLength, Color color) {\n        this.guiRenderState.submitGuiElement(new BraidDashedLineElement(\n            color,\n            thiccness,\n            segmentLength,\n            pipeline,\n            new Matrix3x2f(this.pose()),\n            new ScreenRectangle((int) x1, (int) y1, (int) (x2 - x1), (int) (y2 - y1)),\n            this.scissorStack.peek()\n        ));\n    }\n\n    @FunctionalInterface\n    public interface RectEdgeBuilder {\n        void edge(double x1, double y1, double x2, double y2);\n    }\n\n    @SuppressWarnings(\"ExternalizableWithoutPublicNoArgConstructor\")\n    public static class MatrixStack extends Matrix3x2fStack {\n\n        public MatrixStack(Matrix3x2fc source) {\n            super(16);\n            this.mul(source);\n        }\n\n        @Override\n        public Matrix3x2fStack pushMatrix() {\n            var accessor = (Matrix3x2fStackAccessor) this;\n\n            if (accessor.owo$getCurr() == accessor.owo$getMats().length) {\n                var newMats = new Matrix3x2f[accessor.owo$getMats().length * 2];\n                System.arraycopy(accessor.owo$getMats(), 0, newMats, 0, accessor.owo$getMats().length);\n                for (int idx = newMats.length / 2; idx < newMats.length; idx++) {\n                    newMats[idx] = new Matrix3x2f();\n                }\n\n                accessor.owo$setMats(newMats);\n            }\n\n            return super.pushMatrix();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/BraidHotReloadCallback.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.time.Instant;\nimport java.time.temporal.ChronoUnit;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic final class BraidHotReloadCallback {\n\n    private static final Set<Listener> LISTENERS = new HashSet<>();\n    public static final Logger LOGGER = LoggerFactory.getLogger(\"braid reload agent\");\n\n    public static Listener register() {\n        var listener = new Listener();\n        LISTENERS.add(listener);\n\n        return listener;\n    }\n\n    @ApiStatus.Internal\n    public static void setupComplete() {\n        LOGGER.info(\"setup complete, debounce time is {}ms\", Listener.DEBOUNCE_TIME);\n    }\n\n    @ApiStatus.Internal\n    public static void invoke() {\n        for (var listener : LISTENERS) {\n            listener.triggered.set(true);\n        }\n    }\n\n    public static class Listener {\n\n        private static final int DEBOUNCE_TIME = Integer.getInteger(\"owo.braid.hotswapDebounceTime\", 250);\n\n        private final AtomicBoolean triggered = new AtomicBoolean();\n        private @Nullable Instant lastTriggerTimestamp = null;\n\n        public boolean poll() {\n            if (this.triggered.getAndSet(false)) {\n                this.lastTriggerTimestamp = Instant.now();\n            }\n\n            if (this.lastTriggerTimestamp != null && ChronoUnit.MILLIS.between(this.lastTriggerTimestamp, Instant.now()) > DEBOUNCE_TIME) {\n                this.lastTriggerTimestamp = null;\n                return true;\n            }\n\n            return false;\n        }\n\n        public void unregister() {\n            LISTENERS.remove(this);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/BraidRenderPipelines.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport io.wispforest.owo.Owo;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport org.jetbrains.annotations.ApiStatus;\n\npublic class BraidRenderPipelines {\n    public static final RenderPipeline TEXTURED_DEFAULT = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)\n        .withLocation(Owo.id(\"pipeline/braid_textured_default\"))\n        .build();\n\n    public static final RenderPipeline TEXTURED_NEAREST = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)\n        .withLocation(Owo.id(\"pipeline/braid_textured_nearest\"))\n        .build();\n\n    public static final RenderPipeline TEXTURED_BILINEAR = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)\n        .withLocation(Owo.id(\"pipeline/braid_textured_bilinear\"))\n        .build();\n\n    @ApiStatus.Internal\n    public static void register() {\n        RenderPipelines.register(TEXTURED_DEFAULT);\n        RenderPipelines.register(TEXTURED_NEAREST);\n        RenderPipelines.register(TEXTURED_BILINEAR);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/BraidScreen.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport io.wispforest.owo.braid.core.events.*;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.BraidApp;\nimport io.wispforest.owo.ui.util.DisposableScreen;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\npublic class BraidScreen extends Screen implements DisposableScreen {\n\n    protected final EventBinding eventBinding = new EventBinding.Default();\n    protected final Surface.Default surface = new Surface.Default();\n\n    protected final Settings settings;\n    protected final Widget rootWidget;\n    public AppState state;\n\n    public BraidScreen(Settings settings, Widget rootWidget) {\n        super(Component.empty());\n        this.settings = settings;\n        this.rootWidget = rootWidget;\n    }\n\n    public BraidScreen(Widget rootWidget) {\n        this(new Settings(), rootWidget);\n    }\n\n    @Override\n    protected void init() {\n        super.init();\n\n        if (this.state == null) {\n            var widget = this.settings.useBraidAppWidget\n                ? new BraidApp(this.rootWidget)\n                : this.rootWidget;\n\n            this.state = new AppState(\n                null,\n                AppState.formatName(\"BraidScreen\", this.rootWidget),\n                this.minecraft,\n                this.surface,\n                this.eventBinding,\n                new BraidScreenProvider(this, widget)\n            );\n        }\n    }\n\n    @Override\n    public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {\n        super.render(graphics, mouseX, mouseY, delta);\n\n        this.eventBinding.add(new MouseMoveEvent(mouseX, mouseY));\n        this.state.processEvents(\n            this.minecraft.getDeltaTracker().getGameTimeDeltaTicks()\n        );\n\n        this.state.draw(graphics);\n    }\n\n    @Override\n    public void dispose() {\n        this.state.dispose();\n    }\n\n    @Override\n    public boolean isPauseScreen() {\n        return this.settings.shouldPause;\n    }\n\n    @Override\n    public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {\n        this.eventBinding.add(new MouseButtonPressEvent(click.button(), click.modifiers()));\n        return true;\n    }\n\n    @Override\n    public boolean mouseReleased(MouseButtonEvent click) {\n        this.eventBinding.add(new MouseButtonReleaseEvent(click.button(), click.modifiers()));\n        return true;\n    }\n\n    @Override\n    public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {\n        this.eventBinding.add(new MouseScrollEvent(horizontalAmount, verticalAmount));\n        return true;\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        this.eventBinding.add(new KeyPressEvent(input.key(), input.scancode(), input.modifiers()));\n        return super.keyPressed(input);\n    }\n\n    @Override\n    public boolean keyReleased(KeyEvent input) {\n        this.eventBinding.add(new KeyReleaseEvent(input.key(), input.scancode(), input.modifiers()));\n        return true;\n    }\n\n    @Override\n    public boolean charTyped(CharacterEvent input) {\n        this.eventBinding.add(new CharInputEvent((char) input.codepoint(), input.modifiers()));\n        return true;\n    }\n\n    // ---\n\n    public static @Nullable BraidScreen maybeOf(BuildContext context) {\n        var provider = context.getAncestor(BraidScreenProvider.class);\n        return provider != null ? provider.screen : null;\n    }\n\n    public static class Settings {\n        public boolean shouldPause = true;\n        public boolean useBraidAppWidget = true;\n    }\n}\n\nclass BraidScreenProvider extends InheritedWidget {\n\n    public final BraidScreen screen;\n\n    public BraidScreenProvider(BraidScreen screen, Widget child) {\n        super(child);\n        this.screen = screen;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return false;\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/BraidUtils.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport java.util.function.BiFunction;\n\npublic class BraidUtils {\n    public static <S, T> T fold(Iterable<S> values, T initial, BiFunction<T, S, T> step) {\n        var result = initial;\n        for (var value : values) {\n            result = step.apply(result, value);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/BraidWindow.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.mojang.blaze3d.opengl.GlDebug;\nimport com.mojang.blaze3d.opengl.GlTexture;\nimport com.mojang.blaze3d.pipeline.TextureTarget;\nimport com.mojang.blaze3d.systems.RenderSystem;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.cursor.CursorController;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.core.events.*;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.util.BraidGuiRenderer;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.Minecraft;\nimport org.apache.commons.lang3.mutable.MutableLong;\nimport org.lwjgl.glfw.*;\nimport org.lwjgl.opengl.GL32;\nimport org.lwjgl.system.NativeResource;\n\nimport java.nio.file.InvalidPathException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\n\n// TODO: consider somehow getting notified or polling\n//       for changes in the gui scale option so we can react\n//       instantly when it changes rather than on next resize\npublic class BraidWindow implements Surface {\n\n    public final EventBinding eventBinding = new WindowEventBinding(this);\n\n    public final long handle;\n    private final List<NativeResource> resources = new ArrayList<>();\n\n    private final EventStream<ResizeCallback> onResize = ResizeCallback.newStream();\n    private TextureTarget remoteTarget;\n    private int localFbo;\n\n    public final BraidGuiRenderer guiRenderer;\n\n    private final CursorController cursorController;\n\n    private int framebufferWidth;\n    private int framebufferHeight;\n\n    private int scaledWidth;\n    private int scaledHeight;\n    private int scaleFactor;\n\n    public BraidWindow(long handle) {\n        this.handle = handle;\n        this.cursorController = new CursorController(this.handle);\n\n        this.guiRenderer = new BraidGuiRenderer(Minecraft.getInstance());\n\n        var framebufferWidthOut = new int[1];\n        var framebufferHeightOut = new int[1];\n        GLFW.glfwGetFramebufferSize(this.handle, framebufferWidthOut, framebufferHeightOut);\n\n        this.framebufferWidth = framebufferWidthOut[0];\n        this.framebufferHeight = framebufferHeightOut[0];\n        this.remoteTarget = new TextureTarget(\"braid window\", this.framebufferWidth, this.framebufferHeight, true);\n        this.recreateLocalFbo();\n\n        GLFW.glfwSetWindowCloseCallback(this.handle, this.storeNativeResource(GLFWWindowCloseCallback.create(window -> {\n            this.eventBinding.add(CloseEvent.INSTANCE);\n        })));\n\n        GLFW.glfwSetFramebufferSizeCallback(this.handle, this.storeNativeResource(GLFWFramebufferSizeCallback.create((window, width, height) -> {\n            this.framebufferWidth = width;\n            this.framebufferHeight = height;\n\n            withContext(Minecraft.getInstance().getWindow().handle(), () -> {\n                this.remoteTarget.destroyBuffers();\n                this.remoteTarget = new TextureTarget(\"braid window\", this.framebufferWidth, this.framebufferHeight, true);\n            });\n\n            this.recreateLocalFbo();\n\n            this.onResize.sink().onResize(this.scaledWidth, this.scaledHeight);\n        })));\n\n        GLFW.glfwSetMouseButtonCallback(this.handle, this.storeNativeResource(GLFWMouseButtonCallback.create((window, button, action, mods) -> {\n            this.eventBinding.add(switch (action) {\n                case GLFW.GLFW_PRESS -> new MouseButtonPressEvent(button, new KeyModifiers(mods));\n                case GLFW.GLFW_RELEASE -> new MouseButtonReleaseEvent(button, new KeyModifiers(mods));\n                default -> throw new UnsupportedOperationException(\"incompatible glfw event type\");\n            });\n        })));\n\n        GLFW.glfwSetCursorPosCallback(this.handle, this.storeNativeResource(GLFWCursorPosCallback.create((window, mouseX, mouseY) -> {\n            this.eventBinding.add(new MouseMoveEvent(\n                mouseX / this.scaleFactor,\n                mouseY / this.scaleFactor\n            ));\n        })));\n\n        GLFW.glfwSetScrollCallback(this.handle, this.storeNativeResource(GLFWScrollCallback.create((window, xOffset, yOffset) -> {\n            this.eventBinding.add(new MouseScrollEvent(xOffset, yOffset));\n        })));\n\n        GLFW.glfwSetKeyCallback(this.handle, this.storeNativeResource(GLFWKeyCallback.create((window, key, scancode, action, mods) -> {\n            this.eventBinding.add(switch (action) {\n                case GLFW.GLFW_PRESS, GLFW.GLFW_REPEAT -> new KeyPressEvent(key, scancode, new KeyModifiers(mods));\n                case GLFW.GLFW_RELEASE -> new KeyReleaseEvent(key, scancode, new KeyModifiers(mods));\n                default -> throw new UnsupportedOperationException(\"incompatible glfw event type\");\n            });\n        })));\n\n        GLFW.glfwSetCharModsCallback(this.handle, this.storeNativeResource(GLFWCharModsCallback.create((window, codepoint, mods) -> {\n            this.eventBinding.add(new CharInputEvent((char) codepoint, new KeyModifiers(mods)));\n        })));\n\n        GLFW.glfwSetDropCallback(this.handle, this.storeNativeResource(GLFWDropCallback.create((window, count, names) -> {\n            var paths = new ArrayList<Path>(count);\n\n            for (int pathIdx = 0; pathIdx < count; pathIdx++) {\n                var pathString = GLFWDropCallback.getName(names, pathIdx);\n\n                try {\n                    paths.add(Paths.get(pathString));\n                } catch (InvalidPathException e) {\n                    Owo.LOGGER.error(\"Failed to parse path '{}'\", pathString, e);\n                }\n            }\n\n            if (!paths.isEmpty()) {\n                this.eventBinding.add(new FilesDroppedEvent(paths));\n            }\n        })));\n    }\n\n    private void recreateLocalFbo() {\n        withContext(this.handle, () -> {\n            if (this.localFbo != 0) {\n                GL32.glDeleteFramebuffers(this.localFbo);\n            }\n\n            this.localFbo = GL32.glGenFramebuffers();\n            GL32.glBindFramebuffer(GL32.GL_FRAMEBUFFER, this.localFbo);\n            GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_TEXTURE_2D, ((GlTexture) this.remoteTarget.getColorTexture()).glId(), 0);\n\n            if (GL32.glCheckFramebufferStatus(GL32.GL_FRAMEBUFFER) != GL32.GL_FRAMEBUFFER_COMPLETE) {\n                throw new UnsupportedOperationException(\"Failed to initialize local FBO\");\n            }\n        });\n\n        this.recalculateScale();\n    }\n\n    private void recalculateScale() {\n        var guiScale = Minecraft.getInstance().options.guiScale().get();\n        var forceUnicodeFont = Minecraft.getInstance().options.forceUnicodeFont().get();\n\n        var factor = 1;\n\n        while (\n            factor != guiScale\n                && factor < this.framebufferWidth\n                && factor < this.framebufferHeight\n                && this.framebufferWidth / (factor + 1) >= 320\n                && this.framebufferHeight / (factor + 1) >= 240\n        ) {\n            ++factor;\n        }\n\n        if (forceUnicodeFont && factor % 2 != 0) {\n            ++factor;\n        }\n\n        this.scaleFactor = factor;\n\n        var scaledWidth = (int) ((double) this.framebufferWidth / this.scaleFactor);\n        this.scaledWidth = (double) this.framebufferWidth / this.scaleFactor > (double) scaledWidth ? scaledWidth + 1 : scaledWidth;\n\n        var scaledHeight = (int) ((double) this.framebufferHeight / this.scaleFactor);\n        this.scaledHeight = (double) this.framebufferHeight / this.scaleFactor > (double) scaledHeight ? scaledHeight + 1 : scaledHeight;\n    }\n\n    public static BraidWindow create(String title, int width, int height) {\n        var handleOut = new MutableLong();\n        withContext(0, () -> {\n            GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_OPENGL_API);\n            GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_CREATION_API, GLFW.GLFW_NATIVE_CONTEXT_API);\n            GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);\n            GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2);\n            GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);\n            GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE);\n\n            var handle = GLFW.glfwCreateWindow(width, height, title, 0, Minecraft.getInstance().getWindow().handle());\n\n            if (handle == 0) {\n                throw new UnsupportedOperationException(\"Failed to create a GLFW window\");\n            }\n\n            GLFW.glfwMakeContextCurrent(handle);\n            GLFW.glfwSwapInterval(0);\n\n            GlDebug.enableDebugCallback(Minecraft.getInstance().options.glDebugVerbosity, true, new HashSet<>());\n\n            handleOut.setValue(handle);\n        });\n\n        return new BraidWindow(handleOut.longValue());\n    }\n\n    public static OpenResult open(String title, int width, int height, Widget widget) {\n        var window = create(title, width, height);\n        var app = new AppState(\n            Owo.LOGGER,\n            AppState.formatName(\"BraidWindow\", widget, title),\n            Minecraft.getInstance(),\n            window,\n            window.eventBinding,\n            widget\n        );\n\n        BraidWindowScheduler.add(window, app);\n        return new OpenResult(app, window);\n    }\n\n    // ---\n\n    @Override\n    public void dispose() {\n        GLFW.glfwDestroyWindow(this.handle);\n        this.cursorController.dispose();\n\n        this.guiRenderer.close();\n\n        this.remoteTarget.destroyBuffers();\n\n        for (var resource : this.resources) {\n            resource.free();\n        }\n    }\n\n    // ---\n\n    @Override\n    public int width() {\n        return this.scaledWidth;\n    }\n\n    @Override\n    public int height() {\n        return this.scaledHeight;\n    }\n\n    @Override\n    public double scaleFactor() {\n        return this.scaleFactor;\n    }\n\n    @Override\n    public EventSource<ResizeCallback> onResize() {\n        return this.onResize.source();\n    }\n\n    @Override\n    public CursorStyle currentCursorStyle() {\n        return this.cursorController.currentStyle();\n    }\n\n    @Override\n    public void setCursorStyle(CursorStyle style) {\n        this.cursorController.setStyle(style);\n    }\n\n    // ---\n\n    @Override\n    public void beginRendering() {\n        RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures(\n            this.remoteTarget.getColorTexture(),\n            0xFF000000,\n            this.remoteTarget.getDepthTexture(),\n            1\n        );\n    }\n\n    @Override\n    public void endRendering() {\n        this.guiRenderer.render(new BraidGuiRenderer.Target(\n            this.remoteTarget,\n            this\n        ));\n\n        // ---\n\n        withContext(this.handle, () -> {\n            GL32.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, this.localFbo);\n            GL32.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, 0);\n\n            GL32.glBlitFramebuffer(\n                0, 0, this.framebufferWidth, this.framebufferHeight,\n                0, 0, this.framebufferWidth, this.framebufferHeight,\n                GL32.GL_COLOR_BUFFER_BIT,\n                GL32.GL_NEAREST\n            );\n\n            GLFW.glfwSwapBuffers(this.handle);\n        });\n    }\n\n    // ---\n\n    private <R extends NativeResource> R storeNativeResource(R resource) {\n        this.resources.add(resource);\n        return resource;\n    }\n\n    public static void withContext(long contextHandle, Runnable fn) {\n        var activeContext = GLFW.glfwGetCurrentContext();\n\n        try {\n            GLFW.glfwMakeContextCurrent(contextHandle);\n            fn.run();\n        } finally {\n            GLFW.glfwMakeContextCurrent(activeContext);\n        }\n    }\n\n    // ---\n\n    public static class WindowEventBinding extends EventBinding {\n\n        public final BraidWindow window;\n\n        public WindowEventBinding(BraidWindow window) {\n            this.window = window;\n        }\n\n        @Override\n        public boolean isKeyPressed(int keyCode) {\n            return GLFW.glfwGetKey(this.window.handle, keyCode) == GLFW.GLFW_PRESS;\n        }\n    }\n\n    public record OpenResult(AppState state, BraidWindow window) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/BraidWindowScheduler.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport io.wispforest.owo.ui.event.ClientRenderCallback;\nimport net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;\nimport net.minecraft.client.Minecraft;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class BraidWindowScheduler {\n\n    private static final List<App> APPS = new ArrayList<>();\n\n    public static void add(BraidWindow window, AppState app) {\n        APPS.add(new App(window, app));\n    }\n\n    private static void frame() {\n        for (var app : new ArrayList<>(APPS)) {\n            if (!app.state().running()) {\n                app.state().dispose();\n\n                APPS.remove(app);\n                continue;\n            }\n\n            app.state().processEvents(\n                Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaTicks()\n            );\n\n            app.state().draw(app.surface().guiRenderer.newGraphics(app.state().cursorPosition().x(), app.state().cursorPosition().y()));\n        }\n    }\n\n    static {\n        ClientRenderCallback.BEFORE_SWAP.register(client -> frame());\n        ClientLifecycleEvents.CLIENT_STOPPING.register(client -> {\n            APPS.forEach(app -> app.state().dispose());\n            APPS.clear();\n        });\n    }\n}\n\nrecord App(BraidWindow surface, AppState state) {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Color.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.util.Mth;\n\npublic class Color {\n\n    public static final Color RED = Color.values(1, 0, 0);\n    public static final Color YELLOW = Color.values(1, 1, 0);\n    public static final Color GREEN = Color.values(0, 1, 0);\n    public static final Color AQUA = Color.values(0, 1, 1);\n    public static final Color BLUE = Color.values(0, 0, 1);\n    public static final Color MAGENTA = Color.values(1, 0, 1);\n    public static final Color WHITE = Color.values(1, 1, 1);\n    public static final Color BLACK = Color.values(0, 0, 0);\n\n    //\n\n    public final double r, g, b, a;\n\n    private Color(double r, double g, double b, double a) {\n        this.r = r;\n        this.g = g;\n        this.b = b;\n        this.a = a;\n    }\n\n    // ---\n\n    public Color(int argb) {\n        this(\n            ((argb >> 16) & 0xFF) / 255.0,\n            ((argb >> 8) & 0xFF) / 255.0,\n            (argb & 0xFF) / 255.0,\n            (argb >>> 24) / 255.0\n        );\n    }\n\n    public static Color values(double r, double g, double b, double a) {\n        return new Color(r, g, b, a);\n    }\n\n    public static Color values(double r, double g, double b) {\n        return values(r, g, b, 1);\n    }\n\n    public static Color rgb(int rgb) {\n        return values(\n            ((rgb >> 16) & 0xFF) / 255.0,\n            ((rgb >> 8) & 0xFF) / 255.0,\n            (rgb & 0xFF) / 255.0\n        );\n    }\n\n    public static Color hsv(double hue, double saturation, double value, double alpha) {\n        // we call .5e-7f the magic \"do not turn a hue value of 1f into yellow\" constant\n        return new Color((int) (alpha * 255) << 24 | Mth.hsvToRgb((float) (hue - .5e-7f), (float) saturation, (float) value));\n    }\n\n    public static Color hsv(double hue, double saturation, double value) {\n        return hsv(hue, saturation, value, 1);\n    }\n\n    public static Color formatting(ChatFormatting formatting) {\n        var rgb = formatting.getColor();\n        return rgb(rgb != null ? rgb : 0);\n    }\n\n    public static Color mix(double t, Color a, Color b) {\n        return Color.values(\n            Mth.lerp(t, a.r, b.r),\n            Mth.lerp(t, a.g, b.g),\n            Mth.lerp(t, a.b, b.b),\n            Mth.lerp(t, a.a, b.a)\n        );\n    }\n\n    public static Color randomHue() {\n        return hsv(Math.random(), .75, 1);\n    }\n\n    // ---\n\n    public io.wispforest.owo.ui.core.Color toOwoUi() {\n        return new io.wispforest.owo.ui.core.Color(\n            (float) this.r, (float) this.g, (float) this.b, (float) this.a\n        );\n    }\n\n    public String toHexString(boolean includeAlpha) {\n        return includeAlpha\n            ? String.format(\"#%08X\", this.argb())\n            : String.format(\"#%06X\", this.rgb());\n    }\n\n    //\n\n    public Color withR(double r) {\n        return new Color(r, this.g, this.b, this.a);\n    }\n\n    public Color withG(double g) {\n        return new Color(this.r, g, this.b, this.a);\n    }\n\n    public Color withB(double b) {\n        return new Color(this.r, this.g, b, this.a);\n    }\n\n    public Color withA(double a) {\n        return new Color(this.r, this.g, this.b, a);\n    }\n\n    // ---\n\n    public int rgb() {\n        return (int) (this.r * 255) << 16\n            | (int) (this.g * 255) << 8\n            | (int) (this.b * 255);\n    }\n\n    public int argb() {\n        return (int) (this.a * 255) << 24\n            | (int) (this.r * 255) << 16\n            | (int) (this.g * 255) << 8\n            | (int) (this.b * 255);\n    }\n\n    public float[] hsv() {\n        return this.toOwoUi().hsv();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null || this.getClass() != o.getClass()) return false;\n\n        var other = (Color) o;\n        return this.r == other.r\n            && this.g == other.g\n            && this.b == other.b\n            && this.a == other.a;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Double.hashCode(r);\n        result = 31 * result + Double.hashCode(g);\n        result = 31 * result + Double.hashCode(b);\n        result = 31 * result + Double.hashCode(a);\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/CompoundListenable.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class CompoundListenable extends Listenable {\n\n    protected final Runnable listener = this::notifyListeners;\n    protected final List<Listenable> children = new ArrayList<>();\n\n    public CompoundListenable(Listenable... initialChildren) {\n        for (var child : initialChildren) {\n            this.addChild(child);\n        }\n    }\n\n    public void addChild(Listenable child) {\n        this.children.add(child);\n        child.addListener(this.listener);\n    }\n\n    public void removeChild(Listenable child) {\n        this.children.remove(child);\n        child.removeListener(this.listener);\n    }\n\n    public void clear() {\n        for (var child : this.children) {\n            child.removeListener(this.listener);\n        }\n\n        this.children.clear();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Constraints.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\npublic record Constraints(double minWidth, double minHeight, double maxWidth, double maxHeight) {\n\n    private static final Constraints UNCONSTRAINED = new Constraints(0, 0, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);\n\n    @ApiStatus.Internal\n    @Deprecated(forRemoval = true)\n    public Constraints {}\n\n    public static Constraints unconstrained() {\n        return UNCONSTRAINED;\n    }\n\n    public static Constraints of(double minWidth, double minHeight, double maxWidth, double maxHeight) {\n        return new Constraints(minWidth, minHeight, maxWidth, maxHeight);\n    }\n\n    public static Constraints ofMinWidth(double minWidth) {\n        return new Constraints(minWidth, 0, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);\n    }\n\n    public static Constraints ofMinHeight(double minHeight) {\n        return new Constraints(0, minHeight, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);\n    }\n\n    public static Constraints ofMaxWidth(double maxWidth) {\n        return new Constraints(0, 0, maxWidth, Double.POSITIVE_INFINITY);\n    }\n\n    public static Constraints ofMaxHeight(double maxHeight) {\n        return new Constraints(0, 0, Double.POSITIVE_INFINITY, maxHeight);\n    }\n\n    public static Constraints only(@Nullable Double minWidth, @Nullable Double minHeight, @Nullable Double maxWidth, @Nullable Double maxHeight) {\n        return new Constraints(\n            minWidth != null ? minWidth : 0,\n            minHeight != null ? minHeight : 0,\n            maxWidth != null ? maxWidth : Double.POSITIVE_INFINITY,\n            maxHeight != null ? maxHeight : Double.POSITIVE_INFINITY\n        );\n    }\n\n    public static Constraints tight(Size exactSize) {\n        return new Constraints(exactSize.width(), exactSize.height(), exactSize.width(), exactSize.height());\n    }\n\n    public static Constraints loose(Size maxSize) {\n        return new Constraints(0, 0, maxSize.width(), maxSize.height());\n    }\n\n    public static Constraints tightOnAxis(@Nullable Double horizontal, @Nullable Double vertical) {\n        return only(horizontal, vertical, horizontal, vertical);\n    }\n\n    // ---\n\n    public Constraints withMinWidth(double minWidth) {\n        return new Constraints(minWidth, this.minHeight, this.maxWidth, this.maxHeight);\n    }\n\n    public Constraints withMinHeight(double minHeight) {\n        return new Constraints(this.minWidth, minHeight, this.maxWidth, this.maxHeight);\n    }\n\n    public Constraints withMaxWidth(double maxWidth) {\n        return new Constraints(this.minWidth, this.minHeight, maxWidth, this.maxHeight);\n    }\n\n    public Constraints withMaxHeight(double maxHeight) {\n        return new Constraints(this.minWidth, this.minHeight, this.maxWidth, maxHeight);\n    }\n\n    // ---\n\n    public double minOnAxis(LayoutAxis axis) {\n        return switch (axis) {\n            case HORIZONTAL -> this.minWidth();\n            case VERTICAL -> this.minHeight();\n        };\n    }\n\n    public double maxOnAxis(LayoutAxis axis) {\n        return switch (axis) {\n            case HORIZONTAL -> this.maxWidth();\n            case VERTICAL -> this.maxHeight();\n        };\n    }\n\n    public double maxFiniteOrMinOnAxis(LayoutAxis axis) {\n        return switch (axis) {\n            case HORIZONTAL -> this.maxFiniteOrMinWidth();\n            case VERTICAL -> this.maxFiniteOrMinHeight();\n        };\n    }\n\n    public double maxFiniteOrMinWidth() {\n        return this.hasBoundedWidth() ? this.maxWidth() : this.minWidth();\n    }\n\n    public double maxFiniteOrMinHeight() {\n        return this.hasBoundedHeight() ? this.maxHeight() : this.minHeight();\n    }\n\n    // ---\n\n    public Constraints asLoose() {\n        return this.isLoose() ? this : new Constraints(0, 0, this.maxWidth, this.maxHeight);\n    }\n\n    public Constraints respecting(Constraints other) {\n        if (this.minWidth >= other.minWidth && this.minWidth <= other.maxWidth\n            && this.maxWidth >= other.minWidth && this.maxWidth <= other.maxWidth\n            && this.minHeight >= other.minHeight && this.minHeight <= other.maxHeight\n            && this.maxHeight >= other.minHeight && this.maxHeight <= other.maxHeight) {\n            return this;\n        }\n\n        return new Constraints(\n            Mth.clamp(this.minWidth, other.minWidth, other.maxWidth),\n            Mth.clamp(this.minHeight, other.minHeight, other.maxHeight),\n            Mth.clamp(this.maxWidth, other.minWidth, other.maxWidth),\n            Mth.clamp(this.maxHeight, other.minHeight, other.maxHeight)\n        );\n    }\n\n    public boolean hasLooseWidth() {\n        return this.minWidth == 0;\n    }\n\n    public boolean hasLooseHeight() {\n        return this.minHeight == 0;\n    }\n\n    public boolean hasTightWidth() {\n        return this.minWidth == this.maxWidth;\n    }\n\n    public boolean hasTightHeight() {\n        return this.minHeight == this.maxHeight;\n    }\n\n    public boolean isLoose() {\n        return this.hasLooseWidth() && this.hasLooseHeight();\n    }\n\n    public boolean isTight() {\n        return this.hasTightWidth() && this.hasTightHeight();\n    }\n\n    public boolean hasBoundedWidth() {\n        return this.maxWidth < Double.POSITIVE_INFINITY;\n    }\n\n    public boolean hasBoundedHeight() {\n        return this.maxHeight < Double.POSITIVE_INFINITY;\n    }\n\n    public Size minSize() {\n        return Size.of(this.minWidth, this.minHeight);\n    }\n\n    public Size maxSize() {\n        return Size.of(this.maxWidth, this.maxHeight);\n    }\n\n    public Size maxFiniteOrMinSize() {\n        return Size.of(\n            this.maxFiniteOrMinWidth(),\n            this.maxFiniteOrMinHeight()\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/EventBinding.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.mojang.blaze3d.platform.InputConstants;\nimport io.wispforest.owo.braid.core.events.UserEvent;\nimport net.minecraft.client.Minecraft;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic abstract class EventBinding {\n\n    private final List<EventSlot> bufferedEvents = new ArrayList<>();\n\n    public EventSlot add(UserEvent event) {\n        var slot = new EventSlot(event);\n        this.bufferedEvents.add(slot);\n\n        return slot;\n    }\n\n    List<EventSlot> poll() {\n        var events = new ArrayList<>(this.bufferedEvents);\n        this.bufferedEvents.clear();\n\n        return events;\n    }\n\n    public abstract boolean isKeyPressed(int keyCode);\n\n    public KeyModifiers activeModifiers() {\n        return new KeyModifiers(\n            (this.isKeyPressed(GLFW.GLFW_KEY_LEFT_SHIFT) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_SHIFT) ? GLFW.GLFW_MOD_SHIFT : 0)\n            | (this.isKeyPressed(GLFW.GLFW_KEY_LEFT_CONTROL) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_CONTROL) ? GLFW.GLFW_MOD_CONTROL : 0)\n            | (this.isKeyPressed(GLFW.GLFW_KEY_LEFT_ALT) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_ALT) ? GLFW.GLFW_MOD_ALT : 0)\n            | (this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_SUPER) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_SUPER) ? GLFW.GLFW_MOD_SUPER : 0)\n            | (this.isKeyPressed(GLFW.GLFW_KEY_NUM_LOCK) ? GLFW.GLFW_MOD_NUM_LOCK : 0)\n            | (this.isKeyPressed(GLFW.GLFW_KEY_CAPS_LOCK) ? GLFW.GLFW_MOD_CAPS_LOCK : 0)\n        );\n    }\n\n    public static class EventSlot {\n        final UserEvent event;\n        private boolean handled = false;\n\n        public EventSlot(UserEvent event) {\n            this.event = event;\n        }\n\n        public boolean handled() {\n            return this.handled;\n        }\n\n        void markHandled() {\n            this.handled = true;\n        }\n    }\n\n    // ---\n\n    public static class Headless extends EventBinding {\n        @Override\n        public boolean isKeyPressed(int keyCode) {\n            return false;\n        }\n    }\n\n    public static class Default extends EventBinding {\n        @Override\n        public boolean isKeyPressed(int keyCode) {\n            return InputConstants.isKeyDown(Minecraft.getInstance().getWindow(), keyCode);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Insets.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport org.jetbrains.annotations.ApiStatus;\n\npublic record Insets(double top, double bottom, double left, double right) {\n\n    private static final Insets NONE = new Insets(0, 0, 0, 0);\n\n    @ApiStatus.Internal\n    @Deprecated(forRemoval = true)\n    public Insets {}\n\n    // ---\n\n    public static Insets of(double top, double bottom, double left, double right) {\n        return new Insets(top, bottom, left, right);\n    }\n\n    public static Insets all(double inset) {\n        return new Insets(inset, inset, inset, inset);\n    }\n\n    public static Insets both(double horizontal, double vertical) {\n        return new Insets(vertical, vertical, horizontal, horizontal);\n    }\n\n    public static Insets top(double top) {\n        return new Insets(top, 0, 0, 0);\n    }\n\n    public static Insets bottom(double bottom) {\n        return new Insets(0, bottom, 0, 0);\n    }\n\n    public static Insets left(double left) {\n        return new Insets(0, 0, left, 0);\n    }\n\n    public static Insets right(double right) {\n        return new Insets(0, 0, 0, right);\n    }\n\n    public static Insets vertical(double inset) {\n        return new Insets(inset, inset, 0, 0);\n    }\n\n    public static Insets horizontal(double inset) {\n        return new Insets(0, 0, inset, inset);\n    }\n\n    public static Insets none() {\n        return NONE;\n    }\n\n    // ---\n\n    public Insets withTop(double top) {\n        return new Insets(top, this.bottom, this.left, this.right);\n    }\n\n    public Insets withBottom(double bottom) {\n        return new Insets(this.top, bottom, this.left, this.right);\n    }\n\n    public Insets withLeft(double left) {\n        return new Insets(this.top, this.bottom, left, this.right);\n    }\n\n    public Insets withRight(double right) {\n        return new Insets(this.top, this.bottom, this.left, right);\n    }\n\n    public double horizontal() {\n        return this.left + this.right;\n    }\n\n    public double vertical() {\n        return this.top + this.bottom;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/KeyModifiers.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport it.unimi.dsi.fastutil.ints.IntList;\n\nimport static org.lwjgl.glfw.GLFW.*;\n\npublic record KeyModifiers(int bitMask) {\n    public static final KeyModifiers NONE = new KeyModifiers(0);\n\n    public boolean shift() {\n        return (this.bitMask & GLFW_MOD_SHIFT) != 0;\n    }\n\n    public boolean ctrl() {\n        return (this.bitMask & GLFW_MOD_CONTROL) != 0;\n    }\n\n    public boolean alt() {\n        return (this.bitMask & GLFW_MOD_ALT) != 0;\n    }\n\n    public boolean meta() {\n        return (this.bitMask & GLFW_MOD_SUPER) != 0;\n    }\n\n    public boolean capsLock() {\n        return (this.bitMask & GLFW_MOD_CAPS_LOCK) != 0;\n    }\n\n    public boolean numLock() {\n        return (this.bitMask & GLFW_MOD_NUM_LOCK) != 0;\n    }\n\n    public static boolean isModifier(int keyCode) {\n        return MODIFIER_KEYS.contains(keyCode);\n    }\n\n    public static KeyModifiers both(KeyModifiers a, KeyModifiers b) {\n        return new KeyModifiers(a.bitMask | b.bitMask);\n    }\n\n    public static final IntList MODIFIER_KEYS = IntList.of(\n        GLFW_KEY_LEFT_SHIFT,\n        GLFW_KEY_RIGHT_SHIFT,\n        GLFW_KEY_LEFT_CONTROL,\n        GLFW_KEY_RIGHT_CONTROL,\n        GLFW_KEY_LEFT_ALT,\n        GLFW_KEY_RIGHT_ALT,\n        GLFW_KEY_LEFT_SUPER,\n        GLFW_KEY_RIGHT_SUPER\n    );\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/LayoutAxis.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport java.util.function.Supplier;\n\npublic enum LayoutAxis {\n    HORIZONTAL,\n    VERTICAL;\n\n    public <T> T choose(T horizontal, T vertical) {\n        return switch (this) {\n            case HORIZONTAL -> horizontal;\n            case VERTICAL -> vertical;\n        };\n    }\n\n    public <T> T chooseCompute(Supplier<T> horizontal, Supplier<T> vertical) {\n        return switch (this) {\n            case HORIZONTAL -> horizontal.get();\n            case VERTICAL -> vertical.get();\n        };\n    }\n\n    public Size createSize(double extent, double crossExtent) {\n        return switch (this) {\n            case HORIZONTAL -> Size.of(extent, crossExtent);\n            case VERTICAL -> Size.of(crossExtent, extent);\n        };\n    }\n\n    public LayoutAxis opposite() {\n        return switch (this) {\n            case HORIZONTAL -> VERTICAL;\n            case VERTICAL -> HORIZONTAL;\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Listenable.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic abstract class Listenable {\n\n    protected final List<Runnable> listeners = new ArrayList<>();\n\n    public void addListener(Runnable listener) {\n        this.listeners.add(listener);\n    }\n\n    public void removeListener(Runnable listener) {\n        this.listeners.remove(listener);\n    }\n\n    protected void notifyListeners() {\n        this.listeners.forEach(Runnable::run);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/ListenableValue.java",
    "content": "package io.wispforest.owo.braid.core;\n\npublic class ListenableValue<V> extends Listenable {\n\n    private V value;\n\n    public ListenableValue(V value) {\n        this.value = value;\n    }\n\n    public V value() {\n        return this.value;\n    }\n\n    public void setValue(V value) {\n        this.value = value;\n        this.notifyListeners();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/RelativePosition.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport org.joml.Vector2d;\nimport org.joml.Vector2f;\n\npublic record RelativePosition(BuildContext context, double x, double y) {\n\n    public Vector2d convertTo(BuildContext ancestor) {\n        var contextInstance = context.instance();\n        var ancestorInstance = ancestor.instance();\n\n        if (Owo.DEBUG) {\n            Preconditions.checkArgument(\n                contextInstance.ancestors().contains(ancestorInstance),\n                \"a RelativePosition can only be converted to the coordinate system of an ancestor\"\n            );\n        }\n\n        var coordinates = new Vector2f((float) this.x, (float) this.y);\n        contextInstance.computeTransformFrom(ancestorInstance).invert().transformPosition(coordinates);\n\n        return new Vector2d(coordinates.x, coordinates.y);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Size.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\npublic record Size(double width, double height) {\n\n    private static final Size ZERO = new Size(0, 0);\n\n    @ApiStatus.Internal\n    @Deprecated(forRemoval = true)\n    public Size {}\n\n    // ---\n\n    public static Size zero() {\n        return ZERO;\n    }\n\n    public static Size of(double width, double height) {\n        return new Size(width, height);\n    }\n\n    public static Size square(double sideLength) {\n        return new Size(sideLength, sideLength);\n    }\n\n    public static Size max(Size a, Size b) {\n        return new Size(Math.max(a.width, b.width), Math.max(a.height, b.height));\n    }\n\n    // ---\n\n    public Size withInsets(Insets insets) {\n        return new Size(this.width + insets.horizontal(), this.height + insets.vertical());\n    }\n\n    public Size with(@Nullable Double width, @Nullable Double height) {\n        return new Size(width != null ? width : this.width, height != null ? height : this.height);\n    }\n\n    public Size floor() {\n        return new Size(Math.floor(this.width), Math.floor(this.height));\n    }\n\n    public Size ceil() {\n        return new Size(Math.ceil(this.width), Math.ceil(this.height));\n    }\n\n    public double getExtent(LayoutAxis axis) {\n        return switch (axis) {\n            case HORIZONTAL -> width();\n            case VERTICAL -> height();\n        };\n    }\n\n    public Size constrained(Constraints constraints) {\n        return new Size(\n            Mth.clamp(this.width, constraints.minWidth(), constraints.maxWidth()),\n            Mth.clamp(this.height, constraints.minHeight(), constraints.maxHeight())\n        );\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/Surface.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.mojang.blaze3d.platform.Window;\nimport io.wispforest.owo.braid.core.cursor.CursorController;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.ui.event.WindowResizeCallback;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.Minecraft;\n\npublic interface Surface {\n\n    int width();\n    int height();\n    double scaleFactor();\n\n    EventSource<ResizeCallback> onResize();\n\n    CursorStyle currentCursorStyle();\n    void setCursorStyle(CursorStyle style);\n\n    void beginRendering();\n    void endRendering();\n\n    void dispose();\n\n    class Default implements Surface {\n\n        private static EventStream<ResizeCallback> resizeEvents;\n\n        private final Window window;\n        private final CursorController cursorController;\n\n        public Default() {\n            this.window = Minecraft.getInstance().getWindow();\n            this.cursorController = new CursorController(this.window.handle());\n\n            if (resizeEvents == null) {\n                resizeEvents = ResizeCallback.newStream();\n\n                WindowResizeCallback.EVENT.register((client, resizedWindow) -> {\n                    resizeEvents.sink().onResize(resizedWindow.getGuiScaledWidth(), resizedWindow.getGuiScaledHeight());\n                });\n            }\n        }\n\n        @Override\n        public int width() {\n            return this.window.getGuiScaledWidth();\n        }\n\n        @Override\n        public int height() {\n            return this.window.getGuiScaledHeight();\n        }\n\n        @Override\n        public double scaleFactor() {\n            return this.window.getGuiScale();\n        }\n\n        @Override\n        public EventSource<ResizeCallback> onResize() {\n            return resizeEvents.source();\n        }\n\n        @Override\n        public CursorStyle currentCursorStyle() {\n            return this.cursorController.currentStyle();\n        }\n\n        @Override\n        public void setCursorStyle(CursorStyle style) {\n            this.cursorController.setStyle(style);\n        }\n\n        @Override\n        public void beginRendering() {}\n\n        @Override\n        public void endRendering() {}\n\n        @Override\n        public void dispose() {\n            this.cursorController.dispose();\n        }\n    }\n\n    interface ResizeCallback {\n        void onResize(int newWidth, int newHeight);\n\n        static EventStream<ResizeCallback> newStream() {\n            return new EventStream<>(callbacks -> (newWidth, newHeight) -> {\n                for (var callback : callbacks) {\n                    callback.onResize(newWidth, newHeight);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/TextLayout.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TextLayout {\n\n    public static EditMetrics measure(Font font, String text, Style baseStyle, int maxWidth) {\n        var lines = new ArrayList<Line>();\n\n        font.getSplitter().splitLines(\n            text,\n            maxWidth,\n            baseStyle,\n            false,\n            (style, start, end) -> lines.add(new Line(style, start, end))\n        );\n\n        if (text.endsWith(\"\\n\")) {\n            lines.add(new Line(baseStyle, text.length(), text.length()));\n        }\n\n        if (lines.isEmpty()) {\n            lines.add(new Line(baseStyle, 0, 0));\n        }\n\n        // ---\n\n        var textWidth = 0;\n        var textHeight = 0;\n        var lineMetrics = new ArrayList<LineMetrics>();\n\n        for (var line : lines) {\n            var lineWidth = font.width(line.substring(text));\n            lineMetrics.add(new LineMetrics(line.beginIdx, line.endIdx, lineWidth));\n\n            textWidth = Math.max(textWidth, lineWidth);\n            textHeight += font.lineHeight;\n        }\n\n        return new EditMetrics(textWidth, textHeight, lineMetrics);\n    }\n\n    public record LineMetrics(int beginIdx, int endIdx, double width) {\n        public String substring(String fullContent) {\n            return fullContent.substring(this.beginIdx, this.endIdx);\n        }\n    }\n\n    public record EditMetrics(int width, int height, List<LineMetrics> lineMetrics) {}\n\n    private record Line(Style style, int beginIdx, int endIdx) {\n        public Component substring(String fullContent) {\n            return Component.literal(fullContent.substring(this.beginIdx, this.endIdx)).setStyle(this.style);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/TextureSurface.java",
    "content": "package io.wispforest.owo.braid.core;\n\nimport com.mojang.blaze3d.pipeline.TextureTarget;\nimport com.mojang.blaze3d.systems.RenderSystem;\nimport com.mojang.blaze3d.textures.FilterMode;\nimport com.mojang.blaze3d.textures.GpuTextureView;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.util.BraidGuiRenderer;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.texture.AbstractTexture;\nimport net.minecraft.resources.Identifier;\n\nimport java.util.UUID;\n\npublic class TextureSurface implements Surface {\n\n    private final TextureTarget target;\n    private final EventStream<ResizeCallback> resizeEvents = ResizeCallback.newStream();\n\n    public final TextureSurfaceTexture registeredTexture;\n    public final Identifier registeredTextureId;\n\n    private CursorStyle currentCursorStyle = CursorStyle.NONE;\n\n    public final BraidGuiRenderer guiRenderer;\n\n    public TextureSurface(int width, int height) {\n        this.target = new TextureTarget(\"texture surface\", width, height, true);\n        this.guiRenderer = new BraidGuiRenderer(Minecraft.getInstance());\n\n        this.registeredTexture = new TextureSurfaceTexture();\n        this.registeredTextureId = Owo.id(\"texture_surface_\" + UUID.randomUUID());\n\n        Minecraft.getInstance().getTextureManager().register(this.registeredTextureId, this.registeredTexture);\n    }\n\n    public void resize(int width, int height) {\n        this.target.resize(width, height);\n        this.resizeEvents.sink().onResize(width, height);\n\n        this.registeredTexture.sync();\n    }\n\n    public GpuTextureView texture() {\n        return this.target.getColorTextureView();\n    }\n\n    @Override\n    public int width() {\n        return this.target.width;\n    }\n\n    @Override\n    public int height() {\n        return this.target.height;\n    }\n\n    @Override\n    public double scaleFactor() {\n        return 1;\n    }\n\n    @Override\n    public EventSource<ResizeCallback> onResize() {\n        return this.resizeEvents.source();\n    }\n\n    @Override\n    public CursorStyle currentCursorStyle() {\n        return this.currentCursorStyle;\n    }\n\n    @Override\n    public void setCursorStyle(CursorStyle style) {\n        this.currentCursorStyle = style;\n    }\n\n    // ---\n\n    @Override\n    public void beginRendering() {\n        RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures(\n            this.target.getColorTexture(),\n            0x00000000,\n            this.target.getDepthTexture(),\n            1\n        );\n    }\n\n    @Override\n    public void endRendering() {\n        this.guiRenderer.render(new BraidGuiRenderer.Target(\n            this.target,\n            this\n        ));\n    }\n\n    @Override\n    public void dispose() {\n        this.target.destroyBuffers();\n        Minecraft.getInstance().getTextureManager().release(this.registeredTextureId);\n    }\n\n    // ---\n\n    public class TextureSurfaceTexture extends AbstractTexture {\n\n        public TextureSurfaceTexture() {\n            this.sync();\n            this.sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);\n        }\n\n        private void sync() {\n             this.texture = TextureSurface.this.target.getColorTexture();\n             this.textureView = TextureSurface.this.target.getColorTextureView();\n        }\n\n        @Override\n        public void close() {}\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/cursor/CursorController.java",
    "content": "package io.wispforest.owo.braid.core.cursor;\n\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class CursorController {\n\n    private final Map<CursorStyle, Long> cursors = new HashMap<>();\n    private final long windowHandle;\n\n    private CursorStyle lastCursorStyle = CursorStyle.NONE;\n    private boolean disposed = false;\n\n    public CursorController(long windowHandle) {\n        this.windowHandle = windowHandle;\n    }\n\n    public CursorStyle currentStyle() {\n        return this.lastCursorStyle;\n    }\n\n    public void setStyle(CursorStyle style) {\n        if (this.disposed || this.lastCursorStyle == style) return;\n\n        if (style == CursorStyle.NONE) {\n            GLFW.glfwSetCursor(this.windowHandle, 0);\n        } else {\n            if (!this.cursors.containsKey(style)) {\n                this.cursors.put(style, style.allocate());\n            }\n\n            GLFW.glfwSetCursor(this.windowHandle, this.cursors.get(style));\n        }\n\n        this.lastCursorStyle = style;\n    }\n\n    public void dispose() {\n        if (this.disposed) return;\n\n        for (var ptr : this.cursors.values()) {\n            if (ptr == 0) return;\n            GLFW.glfwDestroyCursor(ptr);\n        }\n\n        this.disposed = true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/cursor/CursorStyle.java",
    "content": "package io.wispforest.owo.braid.core.cursor;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport net.minecraft.util.Mth;\nimport org.joml.Matrix3x2f;\nimport org.lwjgl.glfw.GLFW;\n\npublic sealed interface CursorStyle permits SystemCursorStyle {\n    CursorStyle NONE = new SystemCursorStyle(0);\n    CursorStyle POINTER = new SystemCursorStyle(GLFW.GLFW_ARROW_CURSOR);\n    CursorStyle TEXT = new SystemCursorStyle(GLFW.GLFW_IBEAM_CURSOR);\n    CursorStyle HAND = new SystemCursorStyle(GLFW.GLFW_HAND_CURSOR);\n    CursorStyle MOVE = new SystemCursorStyle(GLFW.GLFW_RESIZE_ALL_CURSOR);\n    CursorStyle CROSSHAIR = new SystemCursorStyle(GLFW.GLFW_CROSSHAIR_CURSOR);\n    CursorStyle HORIZONTAL_RESIZE = new SystemCursorStyle(GLFW.GLFW_HRESIZE_CURSOR);\n    CursorStyle VERTICAL_RESIZE = new SystemCursorStyle(GLFW.GLFW_VRESIZE_CURSOR);\n    CursorStyle NWSE_RESIZE = new SystemCursorStyle(GLFW.GLFW_RESIZE_NWSE_CURSOR);\n    CursorStyle NESW_RESIZE = new SystemCursorStyle(GLFW.GLFW_RESIZE_NESW_CURSOR);\n    CursorStyle NOT_ALLOWED = new SystemCursorStyle(GLFW.GLFW_NOT_ALLOWED_CURSOR);\n\n    long allocate();\n\n    static CursorStyle forDraggingAlong(LayoutAxis axis, Matrix3x2f transform3x2) {\n        // Extract the Z rotation from the transform\n        var rotation = Math.atan2(transform3x2.m01, transform3x2.m11);\n\n        // Convert to degrees\n        rotation = Math.toDegrees(rotation);\n        // apply axis adjustment\n        if (axis == LayoutAxis.VERTICAL) rotation += 90;\n        // Normalize to [0, 180) (because the cursors are symmetric)\n        rotation = Mth.positiveModulo(rotation, 180);\n        // Map to [0, 8)\n        rotation /= 22.5;\n\n        if (rotation < 1 || rotation >= 7) return HORIZONTAL_RESIZE;\n        else if (rotation >= 3 && rotation < 5) return VERTICAL_RESIZE;\n        else if (rotation >= 1 && rotation < 3) return NESW_RESIZE;\n        else return NWSE_RESIZE;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/cursor/SystemCursorStyle.java",
    "content": "package io.wispforest.owo.braid.core.cursor;\n\nimport org.lwjgl.glfw.GLFW;\n\npublic final class SystemCursorStyle implements CursorStyle {\n    public final int glfwId;\n\n    SystemCursorStyle(int glfwId) {\n        this.glfwId = glfwId;\n    }\n\n    @Override\n    public long allocate() {\n        return GLFW.glfwCreateStandardCursor(this.glfwId);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/element/BraidBlockElement.java",
    "content": "package io.wispforest.owo.braid.core.element;\n\nimport com.mojang.blaze3d.platform.Lighting;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.LightTexture;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;\nimport net.minecraft.client.renderer.state.CameraRenderState;\nimport net.minecraft.client.renderer.texture.OverlayTexture;\nimport net.minecraft.world.level.block.RenderShape;\nimport net.minecraft.world.level.block.state.BlockState;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix4f;\n\npublic record BraidBlockElement(\n    BlockState block,\n    @Nullable BlockEntityRenderState entity,\n    Matrix4f transform,\n    Matrix3x2f pose,\n    double width,\n    double height,\n    ScreenRectangle scissorArea\n) implements PictureInPictureRenderState {\n\n    @Override\n    public int x0() {\n        return 0;\n    }\n\n    @Override\n    public int x1() {\n        return (int) this.width;\n    }\n\n    @Override\n    public int y0() {\n        return 0;\n    }\n\n    @Override\n    public int y1() {\n        return (int) this.height;\n    }\n\n    @Override\n    public float scale() {\n        return 1;\n    }\n\n    @Override\n    public Matrix3x2f pose() {\n        return this.pose;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        var bounds = new ScreenRectangle(0, 0, (int) this.width, (int) this.height).transformMaxBounds(this.pose);\n\n        return this.scissorArea != null\n            ? this.scissorArea.intersection(bounds)\n            : bounds;\n    }\n\n    public static class Renderer extends PictureInPictureRenderer<BraidBlockElement> {\n\n        public Renderer(MultiBufferSource.BufferSource vertexConsumers) {\n            super(vertexConsumers);\n        }\n\n        @Override\n        public Class<BraidBlockElement> getRenderStateClass() {\n            return BraidBlockElement.class;\n        }\n\n        @Override\n        @SuppressWarnings(\"NonAsciiCharacters\")\n        protected void renderToTexture(BraidBlockElement state, PoseStack matrices) {\n            Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ENTITY_IN_UI);\n\n            matrices.mulPose(state.transform);\n\n            if (state.block.getRenderShape() != RenderShape.INVISIBLE) {\n                Minecraft.getInstance().getBlockRenderer().renderSingleBlock(\n                    state.block, matrices, bufferSource,\n                    LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY\n                );\n            }\n\n            if (state.entity != null) {\n                var медведь = Minecraft.getInstance().getBlockEntityRenderDispatcher().getRenderer(state.entity);\n                if (медведь != null) {\n                    var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();\n                    медведь.submit(state.entity, matrices, dispatcher.getSubmitNodeStorage(), new CameraRenderState());\n                    dispatcher.renderAllFeatures();\n                }\n            }\n        }\n\n        @Override\n        protected float getTranslateY(int height, int windowScaleFactor) {\n            return height / 2f;\n        }\n\n        @Override\n        protected String getTextureLabel() {\n            return \"owo-ui_block\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/element/BraidDashedLineElement.java",
    "content": "package io.wispforest.owo.braid.core.element;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.vertex.VertexConsumer;\nimport io.wispforest.owo.braid.core.Color;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.TextureSetup;\nimport net.minecraft.client.gui.render.state.GuiElementRenderState;\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2d;\n\npublic record BraidDashedLineElement(\n    Color color,\n    double thiccness,\n    double segmentLength,\n    RenderPipeline pipeline,\n    Matrix3x2f pose,\n    ScreenRectangle bounds,\n    ScreenRectangle scissorArea\n) implements GuiElementRenderState {\n\n    @Override\n    public void buildVertices(VertexConsumer buffer) {\n        var colorArgb = this.color.argb();\n\n        var begin = new Vector2d(this.bounds.left(), this.bounds.top());\n        var end = new Vector2d(this.bounds.right(), this.bounds.bottom());\n\n        var step = end.sub(begin, new Vector2d()).normalize().mul(this.segmentLength);\n        var segmentCount = (int) ((end.distance(begin) + this.segmentLength) / (this.segmentLength * 2));\n\n        var offset = end.sub(begin, new Vector2d()).perpendicular().normalize().mul(this.thiccness * .5d);\n        end.set(begin).add(step);\n\n        step.mul(2);\n\n        for (var i = 0; i < segmentCount; i++) {\n            buffer.addVertexWith2DPose(this.pose, (float) (begin.x + offset.x), (float) (begin.y + offset.y)).setColor(colorArgb);\n            buffer.addVertexWith2DPose(this.pose, (float) (begin.x - offset.x), (float) (begin.y - offset.y)).setColor(colorArgb);\n            buffer.addVertexWith2DPose(this.pose, (float) (end.x - offset.x), (float) (end.y - offset.y)).setColor(colorArgb);\n            buffer.addVertexWith2DPose(this.pose, (float) (end.x + offset.x), (float) (end.y + offset.y)).setColor(colorArgb);\n\n            begin.add(step);\n            end.add(step);\n        }\n    }\n\n    @Override\n    public RenderPipeline pipeline() {\n        return this.pipeline;\n    }\n\n    @Override\n    public TextureSetup textureSetup() {\n        return TextureSetup.noTexture();\n    }\n\n    @Override\n    public ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public ScreenRectangle bounds() {\n        var bounds = this.bounds.transformMaxBounds(this.pose);\n        return this.scissorArea != null ? this.scissorArea.intersection(bounds) : bounds;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/element/BraidEntityElement.java",
    "content": "package io.wispforest.owo.braid.core.element;\n\nimport com.mojang.blaze3d.platform.Lighting;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.entity.EntityRenderDispatcher;\nimport net.minecraft.client.renderer.entity.state.EntityRenderState;\nimport net.minecraft.client.renderer.state.CameraRenderState;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix4f;\nimport org.joml.Quaternionf;\n\npublic record BraidEntityElement(\n    EntityRenderState entityState,\n    Matrix4f transform,\n    Matrix3x2f pose,\n    double width,\n    double height,\n    ScreenRectangle scissorArea\n) implements PictureInPictureRenderState {\n\n    @Override\n    public int x0() {\n        return 0;\n    }\n\n    @Override\n    public int x1() {\n        return (int) this.width;\n    }\n\n    @Override\n    public int y0() {\n        return 0;\n    }\n\n    @Override\n    public int y1() {\n        return (int) this.height;\n    }\n\n    @Override\n    public float scale() {\n        return 1;\n    }\n\n    @Override\n    public Matrix3x2f pose() {\n        return this.pose;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        var bounds = new ScreenRectangle(0, 0, (int) this.width, (int) this.height).transformMaxBounds(this.pose);\n\n        return this.scissorArea != null\n            ? this.scissorArea.intersection(bounds)\n            : bounds;\n    }\n\n    public static class Renderer extends PictureInPictureRenderer<BraidEntityElement> {\n\n        private final EntityRenderDispatcher renderManager = Minecraft.getInstance().getEntityRenderDispatcher();\n\n        public Renderer(MultiBufferSource.BufferSource vertexConsumers) {\n            super(vertexConsumers);\n        }\n\n        @Override\n        public Class<BraidEntityElement> getRenderStateClass() {\n            return BraidEntityElement.class;\n        }\n\n        @Override\n        protected void renderToTexture(BraidEntityElement state, PoseStack matrices) {\n            Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ENTITY_IN_UI);\n\n            matrices.mulPose(state.transform);\n\n            var camera = new CameraRenderState();\n            camera.orientation = state.transform.invert().getUnnormalizedRotation(new Quaternionf());\n\n            var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();\n            this.renderManager.submit(state.entityState, camera, 0, 0, 0, matrices, dispatcher.getSubmitNodeStorage());\n            dispatcher.renderAllFeatures();\n        }\n\n        @Override\n        protected float getTranslateY(int height, int windowScaleFactor) {\n            return 0;\n        }\n\n        @Override\n        protected String getTextureLabel() {\n            return \"owo-entity\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/element/BraidItemElement.java",
    "content": "package io.wispforest.owo.braid.core.element;\n\nimport com.mojang.blaze3d.platform.Lighting;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.LightTexture;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.item.ItemStackRenderState;\nimport net.minecraft.client.renderer.texture.OverlayTexture;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix4fc;\n\npublic record BraidItemElement(\n    ItemStackRenderState item,\n    double width,\n    double height,\n    ScreenRectangle scissorArea,\n    Matrix4fc transform,\n    Matrix3x2f pose\n) implements PictureInPictureRenderState {\n\n    @Override\n    public int x0() {\n        return 0;\n    }\n\n    @Override\n    public int x1() {\n        return (int) this.width;\n    }\n\n    @Override\n    public int y0() {\n        return 0;\n    }\n\n    @Override\n    public int y1() {\n        return (int) this.height;\n    }\n\n    @Override\n    public float scale() {\n        return 1;\n    }\n\n    @Override\n    public Matrix3x2f pose() {\n        return this.pose;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        var bounds = new ScreenRectangle(0, 0, (int) this.width, (int) this.height).transformMaxBounds(this.pose);\n\n        return this.scissorArea != null\n            ? this.scissorArea.intersection(bounds)\n            : bounds;\n    }\n\n    public static class Renderer extends PictureInPictureRenderer<BraidItemElement> {\n\n        public Renderer(MultiBufferSource.BufferSource vertexConsumers) {\n            super(vertexConsumers);\n        }\n\n        @Override\n        public Class<BraidItemElement> getRenderStateClass() {\n            return BraidItemElement.class;\n        }\n\n        @Override\n        protected void renderToTexture(BraidItemElement state, PoseStack matrices) {\n            matrices.scale((float) state.width, (float) -state.height, (float) -Math.min(state.width, state.height));\n            matrices.mulPose(state.transform);\n\n            var notSideLit = !state.item.usesBlockLight();\n            if (notSideLit) {\n                Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ITEMS_FLAT);\n            } else {\n                Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ITEMS_3D);\n            }\n\n            var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();\n            state.item.submit(matrices, dispatcher.getSubmitNodeStorage(), LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY, 0);\n            dispatcher.renderAllFeatures();\n        }\n\n        @Override\n        protected float getTranslateY(int height, int windowScaleFactor) {\n            return height / 2f;\n        }\n\n        @Override\n        protected String getTextureLabel() {\n            return \"owo-item\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/CharInputEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\n\npublic record CharInputEvent(char codepoint, KeyModifiers modifiers) implements UserEvent {\n    public CharInputEvent(char codepoint, int modifiers) {\n        this(codepoint, new KeyModifiers(modifiers));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/CloseEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\npublic enum CloseEvent implements UserEvent {\n    INSTANCE;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/FilesDroppedEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic record FilesDroppedEvent(List<Path> paths) implements UserEvent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/KeyPressEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\n\npublic record KeyPressEvent(int keyCode, int scancode, KeyModifiers modifiers) implements UserEvent {\n    public KeyPressEvent(int keyCode, int scancode, int modifiers) {\n        this(keyCode, scancode, new KeyModifiers(modifiers));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/KeyReleaseEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\n\npublic record KeyReleaseEvent(int keycode, int scancode, KeyModifiers modifiers) implements UserEvent {\n    public KeyReleaseEvent(int keycode, int scancode, int modifiers) {\n        this(keycode, scancode, new KeyModifiers(modifiers));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/MouseButtonPressEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\n\npublic record MouseButtonPressEvent(int button, KeyModifiers modifiers) implements UserEvent {\n    public MouseButtonPressEvent(int button, int modifiers) {\n        this(button, new KeyModifiers(modifiers));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/MouseButtonReleaseEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\n\npublic record MouseButtonReleaseEvent(int button, KeyModifiers modifiers) implements UserEvent {\n    public MouseButtonReleaseEvent(int button, int modifiers) {\n        this(button, new KeyModifiers(modifiers));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/MouseMoveEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\npublic record MouseMoveEvent(double x, double y) implements UserEvent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/MouseScrollEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\npublic record MouseScrollEvent(double xOffset, double yOffset) implements UserEvent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/core/events/UserEvent.java",
    "content": "package io.wispforest.owo.braid.core.events;\n\npublic sealed interface UserEvent permits\n    CloseEvent,\n    CharInputEvent,\n    FilesDroppedEvent,\n    KeyPressEvent,\n    KeyReleaseEvent,\n    MouseButtonPressEvent,\n    MouseButtonReleaseEvent,\n    MouseMoveEvent,\n    MouseScrollEvent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/display/BraidDisplay.java",
    "content": "package io.wispforest.owo.braid.display;\n\nimport com.mojang.blaze3d.pipeline.BlendFunction;\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.EventBinding;\nimport io.wispforest.owo.braid.core.TextureSurface;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.mixin.braid.RenderTypeInvoker;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.client.renderer.SubmitNodeCollector;\nimport net.minecraft.client.renderer.rendertype.RenderSetup;\nimport net.minecraft.client.renderer.rendertype.RenderType;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.function.Function;\n\npublic class BraidDisplay {\n\n    public DisplayQuad quad;\n\n    public final AppState app;\n    public final TextureSurface surface;\n\n    @ApiStatus.Internal\n    public boolean primaryPressed = false;\n    @ApiStatus.Internal\n    public boolean secondaryPressed = false;\n\n    boolean renderAutomatically = false;\n\n    public BraidDisplay(DisplayQuad quad, int surfaceWidth, int surfaceHeight, Widget widget) {\n        this.quad = quad;\n        this.surface = new TextureSurface(surfaceWidth, surfaceHeight);\n        this.app = new AppState(\n            null,\n            AppState.formatName(\"BraidDisplay\", widget),\n            Minecraft.getInstance(),\n            this.surface,\n            new EventBinding.Headless(),\n            widget\n        );\n    }\n\n    public BraidDisplay renderAutomatically() {\n        this.renderAutomatically = true;\n        return this;\n    }\n\n    public void updateAndDrawApp() {\n        var client = this.app.client();\n\n        this.app.processEvents(\n            client.getDeltaTracker().getGameTimeDeltaTicks()\n        );\n\n        this.app.draw(this.surface.guiRenderer.newGraphics(this.app.cursorPosition().x(), this.app.cursorPosition().y()));\n    }\n\n    public void render(PoseStack matrices, SubmitNodeCollector queue, int light) {\n        var layer = RENDER_TYPE.apply(this.surface);\n        queue.submitCustomGeometry(matrices, layer, (matricesEntry, buffer) -> {\n            var normal = this.quad.normal.toVector3f();\n            buffer.addVertex(matricesEntry, 0, 0, 0).setColor(1f, 1f, 1f, 1f).setUv(0, 1).setLight(light).setNormal(matricesEntry, normal);\n            buffer.addVertex(matricesEntry, this.quad.left.toVector3f()).setColor(1f, 1f, 1f, 1f).setUv(0, 0).setLight(light).setNormal(matricesEntry, normal);\n            buffer.addVertex(matricesEntry, this.quad.top.add(this.quad.left).toVector3f()).setColor(1f, 1f, 1f, 1f).setUv(1, 0).setLight(light).setNormal(matricesEntry, normal);\n            buffer.addVertex(matricesEntry, this.quad.top.toVector3f()).setColor(1f, 1f, 1f, 1f).setUv(1, 1).setLight(light).setNormal(matricesEntry, normal);\n        });\n    }\n\n    // ---\n\n    public static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.BLOCK_SNIPPET)\n        .withLocation(Owo.id(\"pipeline/braid_display\"))\n        .withShaderDefine(\"ALPHA_CUTOUT\", 0.1F)\n        .withCull(false)\n        .withBlend(BlendFunction.TRANSLUCENT)\n        .build();\n\n    private static final Function<TextureSurface, RenderType> RENDER_TYPE = surface -> RenderTypeInvoker.owo$of(\n        Owo.id(\"braid_display\").toString(),\n        RenderSetup.builder(PIPELINE)\n            .withTexture(\"Sampler0\", surface.registeredTextureId)\n            .useLightmap()\n            .createRenderSetup()\n    );\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/display/BraidDisplayBinding.java",
    "content": "package io.wispforest.owo.braid.display;\n\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport io.wispforest.owo.braid.core.events.MouseMoveEvent;\nimport net.minecraft.client.renderer.LightTexture;\nimport net.minecraft.client.renderer.SubmitNodeCollector;\nimport net.minecraft.client.renderer.state.CameraRenderState;\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2dc;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class BraidDisplayBinding {\n\n    private static final List<BraidDisplay> ACTIVE_DISPLAYS = new ArrayList<>();\n\n    // ---\n\n    public static void activate(BraidDisplay display) {\n        ACTIVE_DISPLAYS.add(display);\n    }\n\n    public static void deactivate(BraidDisplay display) {\n        ACTIVE_DISPLAYS.remove(display);\n    }\n\n    // ---\n\n    public static @Nullable DisplayHitResult targetDisplay;\n\n    @ApiStatus.Internal\n    public static @Nullable DisplayHitResult queryTargetDisplay(Vec3 rayOrigin, Vec3 rayDirection) {\n        DisplayHitResult closestResult = null;\n        double closestRayOffset = Double.POSITIVE_INFINITY;\n\n        for (var display : ACTIVE_DISPLAYS) {\n            var result = display.quad.hitTest(rayOrigin, rayDirection);\n            if (result == null || result.t() >= closestRayOffset) continue;\n\n            closestResult = new DisplayHitResult(display, result.point());\n            closestRayOffset = result.t();\n        }\n\n        return closestResult;\n    }\n\n    @ApiStatus.Internal\n    public static void onDisplayHit(DisplayHitResult targetDisplay) {\n        var app = targetDisplay.display.app;\n\n        var cursorX = targetDisplay.point.x() * app.surface.width();\n        var cursorY = targetDisplay.point.y() * app.surface.height();\n\n        app.eventBinding.add(new MouseMoveEvent(cursorX, cursorY));\n    }\n\n    @ApiStatus.Internal\n    public static void updateAndDrawDisplays() {\n        for (var display : ACTIVE_DISPLAYS) {\n            display.updateAndDrawApp();\n        }\n    }\n\n    @ApiStatus.Internal\n    public static void renderAutomaticDisplays(PoseStack matrices, CameraRenderState camera, SubmitNodeCollector nodeCollector) {\n        for (var display : ACTIVE_DISPLAYS) {\n            if (!display.renderAutomatically) continue;\n\n            matrices.pushPose();\n            matrices.translate(display.quad.pos.subtract(camera.pos));\n\n            display.render(matrices, nodeCollector, LightTexture.FULL_BRIGHT);\n\n            matrices.popPose();\n        }\n    }\n\n    // ---\n\n    public record DisplayHitResult(BraidDisplay display, Vector2dc point) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/display/DisplayQuad.java",
    "content": "package io.wispforest.owo.braid.display;\n\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\nimport org.joml.Vector2dc;\n\npublic final class DisplayQuad {\n    public final Vec3 pos;\n    public final Vec3 top;\n    public final Vec3 left;\n    public final Vec3 normal;\n\n    public DisplayQuad(Vec3 pos, Vec3 top, Vec3 left) {\n        this.pos = pos;\n        this.top = top;\n        this.left = left;\n        this.normal = this.left.cross(this.top);\n    }\n\n    public Vec3 unproject(Vector2dc point) {\n        return this.pos.add(this.top.scale(point.x())).add(this.left.scale(point.y()));\n    }\n\n    public @Nullable HitTestResult hitTest(Vec3 origin, Vec3 direction) {\n        var t = this.pos.subtract(origin).dot(this.normal) / direction.dot(this.normal);\n        if (t < 0) return null;\n\n        var candidatePoint = origin.add(direction.scale(t)).subtract(this.pos);\n\n        var widthSquared = this.top.lengthSqr();\n        var heightSquared = this.left.lengthSqr();\n\n        var point = new Vector2d(\n            candidatePoint.dot(this.top) / widthSquared,\n            candidatePoint.dot(this.left) / heightSquared\n        );\n\n        return point.x > 0 && point.x < 1 && point.y > 0 && point.y < 1\n            ? new HitTestResult(point, t)\n            : null;\n    }\n\n    public record HitTestResult(Vector2dc point, double t) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/BuildContext.java",
    "content": "package io.wispforest.owo.braid.framework;\n\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport org.jetbrains.annotations.Nullable;\n\npublic interface BuildContext {\n    <T> @Nullable T getAncestor(Class<T> ancestorClass, Object inheritedKey);\n\n    default <T> @Nullable T getAncestor(Class<T> ancestorClass) {\n        return this.getAncestor(ancestorClass, ancestorClass);\n    }\n\n    <T> @Nullable T dependOnAncestor(Class<T> ancestorClass, Object inheritedKey, @Nullable Object dependency);\n\n    default <T> @Nullable T dependOnAncestor(Class<T> ancestorClass, Object inheritedKey) {\n        return this.dependOnAncestor(ancestorClass, inheritedKey, null);\n    }\n\n    default <T> @Nullable T dependOnAncestor(Class<T> ancestorClass) {\n        return this.dependOnAncestor(ancestorClass, ancestorClass);\n    }\n\n    /// To prevent excessive IDE warnings, the return type of this\n    /// getter is not annotated `@Nullable` even though if it is called\n    /// before this context has been laid out, it will (correctly)\n    /// return null\n    WidgetInstance<?> instance();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/CustomWidgetTransform.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.*;\n\npublic class CustomWidgetTransform extends WidgetTransform {\n\n    protected @Nullable Matrix3x2f toParent;\n    protected @Nullable Matrix3x2f toWidget;\n\n    private boolean applyAtCenter = true;\n    private Matrix3x2f matrix = new Matrix3x2f();\n\n    public void setMatrix(Matrix3x2f matrix) {\n        this.setState(() -> this.matrix = matrix);\n    }\n\n    public Matrix3x2f matrix() {\n        return this.matrix;\n    }\n\n    public void setApplyAtCenter(boolean applyToCenter) {\n        this.setState(() -> this.applyAtCenter = applyToCenter);\n    }\n\n    public boolean applyAtCenter() {\n        return this.applyAtCenter;\n    }\n\n    protected Matrix3x2fc toParent() {\n        if (this.toParent == null) {\n            if (this.applyAtCenter) {\n                this.toParent = new Matrix3x2f()\n                    .translate((float) (this.x + this.width / 2), (float) (this.y + this.height / 2))\n                    .mul(this.matrix)\n                    .translate((float) (-this.width / 2), (float) (-this.height / 2));\n            } else {\n                this.toParent = new Matrix3x2f()\n                    .translate((float) this.x, (float) this.y)\n                    .mul(this.matrix);\n            }\n        }\n\n        return this.toParent;\n    }\n\n    protected Matrix3x2fc toWidget() {\n        if (this.toWidget == null) {\n            this.toWidget = new Matrix3x2f(this.toParent()).invert();\n        }\n\n        return this.toWidget;\n    }\n\n    @Override\n    public void transformToParent(Matrix3x2f mat) {\n        mat.mul(this.toParent());\n    }\n\n    @Override\n    public void transformToParent(Matrix3x2fStack matrices) {\n        matrices.mul(this.toParent());\n    }\n\n    @Override\n    public void transformToWidget(Matrix3x2f mat) {\n        mat.mul(this.toWidget());\n    }\n\n    @Override\n    public void transformToWidget(Matrix3x2fStack matrices) {\n        matrices.mul(this.toWidget());\n    }\n\n    @Override\n    public void toParentCoordinates(Vector2d vec) {\n        var vec2f = new Vector2f(vec);\n        this.toParent().transformPosition(vec2f);\n\n        vec.set(vec2f.x, vec2f.y);\n    }\n\n    @Override\n    public void toWidgetCoordinates(Vector2d vec) {\n        var vec2f = new Vector2f(vec);\n        this.toWidget().transformPosition(vec2f);\n\n        vec.set(vec2f.x, vec2f.y);\n    }\n\n    @Override\n    public void recompute() {\n        super.recompute();\n        this.toParent = null;\n        this.toWidget = null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/Hit.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\npublic record Hit(WidgetInstance<?> instance, double x, double y) {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/HitTestState.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport com.google.common.collect.FluentIterable;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayDeque;\nimport java.util.Deque;\nimport java.util.Iterator;\nimport java.util.function.Predicate;\n\npublic class HitTestState {\n    private final Deque<Hit> hits = new ArrayDeque<>();\n\n    public boolean anyHit() {\n        return !this.hits.isEmpty();\n    }\n\n    public Hit firstHit() {\n        return this.hits.getFirst();\n    }\n\n    public Iterable<Hit> trace() {\n        return this.hits;\n    }\n\n    public Iterable<Hit> occludedTrace() {\n        return new Iterable<>() {\n            @Override\n            public @NotNull Iterator<Hit> iterator() {\n                var inner = HitTestState.this.hits.iterator();\n\n                return new Iterator<>() {\n                    private boolean encounteredBoundary = false;\n\n                    @Override\n                    public boolean hasNext() {\n                        return inner.hasNext() && !this.encounteredBoundary;\n                    }\n\n                    @Override\n                    public Hit next() {\n                        var next = inner.next();\n                        if ((next.instance().flags & WidgetInstance.FLAG_HIT_TEST_BOUNDARY) != 0) {\n                            this.encounteredBoundary = true;\n                        }\n\n                        return next;\n                    }\n                };\n            }\n        };\n    }\n\n    public @Nullable Hit firstWhere(Predicate<Hit> predicate) {\n        return FluentIterable.from(this.occludedTrace()).firstMatch(predicate::test).orNull();\n    }\n\n    public void addHit(WidgetInstance<?> instance, double x, double y) {\n        this.hits.addFirst(new Hit(instance, x, y));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/InspectorProperty.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport net.minecraft.network.chat.Component;\n\npublic record InspectorProperty(Component name, Component value) {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/InstanceHost.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport io.wispforest.owo.braid.widgets.basic.LayoutBuilder;\nimport net.minecraft.client.Minecraft;\nimport org.joml.Vector2dc;\n\npublic interface InstanceHost {\n    Minecraft client();\n\n    /// Schedule a [WidgetInstance#layout] invocation for `instance`,\n    /// to be executed during the next layout pass.\n    ///\n    /// This function must generally not be called during a layout pass\n    /// unless [#notifySubtreeRebuild] has been invoked first since\n    /// otherwise we run the risk of laying out some instances twice\n    void scheduleLayout(WidgetInstance<?> instance);\n\n    /// Notify the layout scheduler that a widget or proxy subtree\n    /// of the current element is (likely) about to rebuild and\n    /// subsequently [#scheduleLayout] may be invoked during the\n    /// current layout pass\n    ///\n    /// This is used to implement the [LayoutBuilder] mechanism\n    void notifySubtreeRebuild();\n\n    void schedulePostLayoutCallback(Runnable callback);\n\n    Vector2dc cursorPosition();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/LeafWidgetInstance.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\n\npublic abstract class LeafWidgetInstance<T extends InstanceWidget> extends WidgetInstance<T> {\n\n    public LeafWidgetInstance(T widget) {\n        super(widget);\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/MouseListener.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport org.jetbrains.annotations.Nullable;\n\npublic interface MouseListener {\n    default @Nullable CursorStyle cursorStyleAt(double x, double y) {\n        return null;\n    }\n\n    default boolean onMouseDown(double x, double y, int button, KeyModifiers modifiers) {\n        return false;\n    }\n    default boolean onMouseUp(double x, double y, int button, KeyModifiers modifiers) {\n        return false;\n    }\n\n    default void onMouseEnter() {}\n    default void onMouseMove(double toX, double toY) {}\n    default void onMouseExit() {}\n    default void onMouseDragStart(int button, KeyModifiers modifiers) {}\n    default void onMouseDrag(double x, double y, double dx, double dy) {}\n    default void onMouseDragEnd() {}\n\n    default boolean onMouseScroll(double x, double y, double horizontal, double vertical) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/MultiChildWidgetInstance.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.BraidUtils;\nimport io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.OptionalDouble;\n\npublic abstract class MultiChildWidgetInstance<T extends MultiChildInstanceWidget> extends WidgetInstance<T> {\n\n    public List<WidgetInstance<?>> children = new ArrayList<>();\n\n    public MultiChildWidgetInstance(T widget) {\n        super(widget);\n    }\n\n    @Override\n    public void draw(BraidGraphics graphics) {\n        for (var child : this.children) {\n            this.drawChild(graphics, child);\n        }\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {\n        for (var child : this.children) {\n            visitor.visit(child);\n        }\n    }\n\n    public void insertChild(int index, WidgetInstance<?> child) {\n        this.children.set(index, this.adopt(child));\n        this.markNeedsLayout();\n    }\n\n    // ---\n\n    protected OptionalDouble computeFirstBaselineOffset() {\n        for (var child : this.children) {\n            var childBaseline = child.getBaselineOffset();\n            if (childBaseline.isEmpty()) continue;\n\n            return OptionalDouble.of(childBaseline.getAsDouble() + child.transform.y);\n        }\n\n        return OptionalDouble.empty();\n    }\n\n    protected OptionalDouble computeHighestBaselineOffset() {\n        return BraidUtils.fold(this.children, null, (acc, child) -> {\n            var childBaseline = child.getBaselineOffset();\n            if (childBaseline.isEmpty()) return acc;\n\n            return baselineMin(acc, OptionalDouble.of(childBaseline.getAsDouble() + child.transform.y));\n        });\n    }\n\n    private static OptionalDouble baselineMin(OptionalDouble a, OptionalDouble b) {\n        if (a.isEmpty()) return b;\n        if (b.isEmpty()) return a;\n        return a.getAsDouble() <= b.getAsDouble() ? a : b;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/OptionalChildWidgetInstance.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.OptionalDouble;\n\npublic abstract class OptionalChildWidgetInstance<T extends InstanceWidget> extends WidgetInstance<T> {\n\n    protected @Nullable WidgetInstance<?> child;\n\n    public OptionalChildWidgetInstance(T widget) {\n        super(widget);\n    }\n\n    @Override\n    public void draw(BraidGraphics graphics) {\n        if (this.child != null) {\n            this.drawChild(graphics, this.child);\n        }\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {\n        if (this.child != null) {\n            visitor.visit(this.child);\n        }\n    }\n\n    public WidgetInstance<?> child() {\n        Preconditions.checkNotNull(this.child, \"tried to retrieve child of SingleChildWidgetInstance before it was set\");\n        return this.child;\n    }\n\n    public void setChild(@Nullable WidgetInstance<?> value) {\n        if (value == this.child) return;\n\n        this.child = this.adopt(value);\n        this.markNeedsLayout();\n    }\n\n    public static abstract class ShrinkWrap<T extends InstanceWidget> extends OptionalChildWidgetInstance<T> {\n\n        public ShrinkWrap(T widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            this.sizeToChild(constraints, this.child);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.child != null ? this.child.getIntrinsicWidth(height) : 0;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.child != null ? this.child.getIntrinsicHeight(width) : 0;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child != null ? this.child.getBaselineOffset() : OptionalDouble.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/SingleChildWidgetInstance.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\n\nimport java.util.OptionalDouble;\n\npublic abstract class SingleChildWidgetInstance<T extends InstanceWidget> extends WidgetInstance<T> {\n\n    protected WidgetInstance<?> child;\n\n    public SingleChildWidgetInstance(T widget) {\n        super(widget);\n    }\n\n    @Override\n    public void draw(BraidGraphics graphics) {\n        this.drawChild(graphics, this.child);\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {\n        visitor.visit(this.child);\n    }\n\n    public WidgetInstance<?> child() {\n        Preconditions.checkNotNull(this.child, \"tried to retrieve child of SingleChildWidgetInstance before it was set\");\n        return this.child;\n    }\n\n    public void setChild(WidgetInstance<?> value) {\n        if (value == this.child) return;\n\n        this.child = this.adopt(value);\n        this.markNeedsLayout();\n    }\n\n    public static abstract class ShrinkWrap<T extends InstanceWidget> extends SingleChildWidgetInstance<T> {\n\n        public ShrinkWrap(T widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            this.sizeToChild(constraints, this.child);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.child.getIntrinsicWidth(height);\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.child.getIntrinsicHeight(width);\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child.getBaselineOffset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/TooltipProvider.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.network.chat.Style;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\npublic interface TooltipProvider {\n    @Nullable List<ClientTooltipComponent> getTooltipComponentsAt(double x, double y);\n\n    @Nullable\n    default Style getStyleAt(double x, double y) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/WidgetInstance.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport it.unimi.dsi.fastutil.objects.Object2DoubleMap;\nimport it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap;\nimport net.minecraft.world.phys.AABB;\nimport org.jetbrains.annotations.MustBeInvokedByOverriders;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2d;\nimport org.joml.Vector2f;\n\nimport java.util.*;\n\npublic abstract class WidgetInstance<T extends InstanceWidget> implements Comparable<WidgetInstance<?>> {\n    public static final int FLAG_HIT_TEST_BOUNDARY = 0b1;\n\n    public final WidgetTransform transform = this.createTransform();\n\n    public @Nullable Object parentData;\n    public int flags = 0;\n    private int depth = 0;\n\n    private InstanceHost host;\n    private WidgetInstance<?> parent;\n\n    protected T widget;\n\n    // ---\n\n    public boolean debugHighlighted = false;\n    public boolean debugDrawVisualizers = false;\n\n    public boolean debugParentHasDependency() {\n        //noinspection OptionalAssignedToNull\n        return !this.intrinsicSizeCache.isEmpty() || this.baselineOffsetCache != null;\n    }\n\n    // ---\n\n    private @Nullable Constraints constraints;\n    private boolean needsLayout = false;\n    private @Nullable WidgetInstance<?> relayoutBoundary;\n\n    public WidgetInstance(T widget) {\n        this.widget = widget;\n    }\n\n    protected WidgetTransform createTransform() {\n        return new WidgetTransform();\n    }\n\n    // ---\n\n    public final Size layout(Constraints constraints) {\n        if (!this.needsLayout && Objects.equals(constraints, this.constraints)) {\n            return this.transform.toSize();\n        }\n\n        this.constraints = constraints;\n        this.relayoutBoundary = constraints.isTight() || this.parent == null ? this : this.parent.relayoutBoundary;\n\n        this.doLayout(constraints);\n        this.needsLayout = false;\n\n        return this.transform.toSize();\n    }\n\n    protected abstract void doLayout(Constraints constraints);\n\n    protected abstract double measureIntrinsicWidth(double height);\n    protected abstract double measureIntrinsicHeight(double width);\n\n    private final Object2DoubleMap<IntrinsicCacheKey> intrinsicSizeCache = new Object2DoubleOpenHashMap<>();\n\n    public double getIntrinsicWidth(double height) {\n        return this.intrinsicSizeCache.computeIfAbsent(new IntrinsicCacheKey(LayoutAxis.HORIZONTAL, height), ($) -> this.measureIntrinsicWidth(height));\n    }\n\n    public double getIntrinsicHeight(double width) {\n        return this.intrinsicSizeCache.computeIfAbsent(new IntrinsicCacheKey(LayoutAxis.VERTICAL, width), ($) -> this.measureIntrinsicHeight(width));\n    }\n\n    protected abstract OptionalDouble measureBaselineOffset();\n\n    private @Nullable OptionalDouble baselineOffsetCache;\n    public OptionalDouble getBaselineOffset() {\n        //noinspection OptionalAssignedToNull\n        if (this.baselineOffsetCache != null) return this.baselineOffsetCache;\n        return this.baselineOffsetCache = this.measureBaselineOffset();\n    }\n\n    // ---\n\n    public abstract void draw(BraidGraphics graphics);\n\n    public abstract void visitChildren(Visitor visitor);\n\n    // ---\n\n    public void attachHost(InstanceHost host) {\n        this.host = host;\n\n        var callback = POST_ATTACH_CALLBACKS.remove(this);\n        if (callback != null) callback.run();\n\n        this.visitChildren(child -> child.attachHost(host));\n    }\n\n    protected <W extends @Nullable WidgetInstance<?>> W adopt(W child) {\n        if (child == null || ((WidgetInstance<?>) child).parent == this) return child;\n\n        child.setDepth(this.depth + 1);\n        ((WidgetInstance<?>) child).parent = this;\n        if (this.host != null) {\n            child.attachHost(this.host);\n        }\n\n        return child;\n    }\n\n    // ---\n\n    public List<InspectorProperty> debugListInspectorProperties() {\n        return List.of();\n    }\n\n    public boolean debugHasVisualizers() {\n        return false;\n    }\n\n    protected void debugDrawVisualizers(BraidGraphics graphics) {}\n\n    // ---\n\n    protected void drawChild(BraidGraphics ctx, WidgetInstance<?> child) {\n        ctx.push();\n        child.transform.transformToParent(ctx.pose());\n        child.draw(ctx);\n\n        if (child.debugHasVisualizers() && child.debugDrawVisualizers) {\n            child.debugDrawVisualizers(ctx);\n        }\n\n        if (child.debugHighlighted) {\n            NinePatchTexture.draw(\n                Owo.id(\"braid_debug_highlighted\"),\n                ctx,\n                0, 0, (int) child.transform.width(), (int) child.transform.height(),\n                Color.ofRgb(0x00FFD1)\n            );\n        }\n\n        ctx.pop();\n    }\n\n    protected void sizeToChild(Constraints constraints, @Nullable WidgetInstance<?> child) {\n        if (child == null) {\n            this.transform.setSize(constraints.minSize());\n        } else {\n            var childSize = child.layout(constraints);\n            this.transform.setSize(childSize);\n        }\n    }\n\n    public void clearLayoutCache(boolean recursive) {\n        this.needsLayout = true;\n\n        if (recursive) {\n            this.visitChildren(child -> child.clearLayoutCache(true));\n        }\n    }\n\n    @SuppressWarnings(\"OptionalAssignedToNull\")\n    public void markNeedsLayout() {\n        this.needsLayout = true;\n\n        var parentHasDependency = !this.intrinsicSizeCache.isEmpty() || this.baselineOffsetCache != null;\n        this.intrinsicSizeCache.clear();\n        this.baselineOffsetCache = null;\n\n        if (!parentHasDependency && this.isRelayoutBoundary()) {\n            if (this.host != null) this.host.scheduleLayout(this);\n        } else {\n            if (this.parent != null) this.parent.markNeedsLayout();\n        }\n    }\n\n    private boolean debugDisposed = false;\n\n    @MustBeInvokedByOverriders\n    public void dispose() {\n        Preconditions.checkState(!this.debugDisposed, \"tried to dispose a widget instance twice\");\n        this.debugDisposed = true;\n\n        this.parent = null;\n    }\n\n    // ---\n\n    public List<WidgetInstance<?>> ancestors() {\n        var result = new ArrayList<WidgetInstance<?>>();\n        var ancestor = this.parent;\n\n        while (ancestor != null) {\n            result.add(ancestor);\n            ancestor = ancestor.parent;\n        }\n\n        return result;\n    }\n\n    public void hitTest(double x, double y, HitTestState state) {\n        if (this.hitTestSelf(x, y)) {\n            state.addHit(this, x, y);\n        }\n\n        var coordinates = new Vector2d();\n        this.visitChildren(child -> {\n            coordinates.set(x, y);\n            child.transform.toWidgetCoordinates(coordinates);\n\n            child.hitTest(coordinates.x, coordinates.y, state);\n        });\n    }\n\n    protected boolean hitTestSelf(double x, double y) {\n        return x >= 0 && x < this.transform.width && y >= 0 && y < this.transform.height;\n    }\n\n    public Matrix3x2f computeGlobalTransform() {\n        return this.computeTransformFrom(null);\n    }\n\n    public Matrix3x2f computeTransformFrom(@Nullable WidgetInstance<?> ancestor) {\n        var result = new Matrix3x2f();\n\n        this.transform.transformToWidget(result);\n\n        for (var step : this.ancestors()) {\n            if (step == ancestor) break;\n            step.transform.transformToWidget(result);\n        }\n\n        return result;\n    }\n\n    public AABB computeGlobalBounds() {\n        var global = this.parent != null ? this.parent.computeGlobalTransform().invert() : new Matrix3x2f();\n\n        var min = global.transformPosition(new Vector2f((float) this.transform.x, (float) this.transform.y));\n        var max = global.transformPosition(new Vector2f((float) (this.transform.x + this.transform.width), (float) (this.transform.y + this.transform.height)));\n\n        return new AABB(min.x, min.y, 0, max.x, max.y, 0);\n    }\n\n    public Vector2d computeGlobalPosition() {\n        var global = this.parent != null ? this.parent.computeGlobalTransform().invert() : new Matrix3x2f();\n\n        var pos = global.transformPosition(new Vector2f((float) this.transform.x, (float) this.transform.y));\n        return new Vector2d(pos.x, pos.y);\n    }\n\n    // ---\n\n\n    public @Nullable Constraints constraints() {\n        return this.constraints;\n    }\n\n    public int depth() {\n        return this.depth;\n    }\n\n    public void setDepth(int depth) {\n        if (this.depth == depth) return;\n\n        this.depth = depth;\n        this.visitChildren(child -> child.setDepth(this.depth + 1));\n    }\n\n    /// To prevent excessive IDE warnings, the return type of this\n    /// getter is not annotated `@Nullable` even though if it is called\n    /// before this instance is adopted it will (correctly) return null\n    public InstanceHost host() {\n        return this.host;\n    }\n\n    public boolean needsLayout() {\n        return this.needsLayout;\n    }\n\n    public boolean isRelayoutBoundary() {\n        return this.relayoutBoundary == this;\n    }\n\n    public boolean hasParent() {\n        return this.parent != null;\n    }\n\n    public void setWidget(T widget) {\n        this.widget = widget;\n    }\n\n    public T widget() {\n        return this.widget;\n    }\n\n    public WidgetInstance<?> parent() {\n        return this.parent;\n    }\n\n    // ---\n\n    private static final WeakHashMap<WidgetInstance<?>, Runnable> POST_ATTACH_CALLBACKS = new WeakHashMap<>();\n    public static void addPostAttachCallback(WidgetInstance<?> instance, Runnable callback) {\n        POST_ATTACH_CALLBACKS.put(instance, callback);\n    }\n\n    // ---\n\n    @Override\n    public int compareTo(@NotNull WidgetInstance<?> o) {\n        return Integer.compare(this.depth, o.depth);\n    }\n\n    // ---\n\n    @FunctionalInterface\n    public interface Visitor {\n        void visit(WidgetInstance<?> child);\n    }\n}\n\nrecord IntrinsicCacheKey(LayoutAxis axis, double crossExtent) {}\n\n//enum Visitors implements WidgetInstance.Visitor {\n//    MARK_NEEDS_LAYOUT(WidgetInstance::markNeedsLayout);\n//\n//    private final WidgetInstance.Visitor delegate;\n//\n//    Visitors(WidgetInstance.Visitor delegate) {\n//        this.delegate = delegate;\n//    }\n//\n//    @Override\n//    public void visit(WidgetInstance<?> child) {\n//        this.delegate.visit(child);\n//    }\n//}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/instance/WidgetTransform.java",
    "content": "package io.wispforest.owo.braid.framework.instance;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.core.Size;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix3x2fStack;\nimport org.joml.Vector2d;\n\npublic class WidgetTransform {\n    protected double x = 0, y = 0;\n    protected double width = 0, height = 0;\n\n    public void setX(double x) {\n        setState(() -> this.x = x);\n    }\n\n    public double x() {\n        return this.x;\n    }\n\n    public void setY(double y) {\n        setState(() -> this.y = y);\n    }\n\n    public double y() {\n        return this.y;\n    }\n\n    public void setWidth(double width) {\n        setState(() -> {\n            if (Double.isInfinite(width)) {\n                this.width = 69420;\n                Owo.LOGGER.error(\"A widget transform received infinite width, clamping to 69420. This should never happen\");\n            } else {\n                this.width = width;\n            }\n        });\n    }\n\n    public double width() {\n        return this.width;\n    }\n\n    public void setHeight(double height) {\n        setState(() -> {\n            if (Double.isInfinite(height)) {\n                this.height = 69420;\n                Owo.LOGGER.error(\"A widget transform received infinite height, clamping to 69420. This should never happen\");\n            } else {\n                this.height = height;\n            }\n        });\n    }\n\n    public double height() {\n        return this.height;\n    }\n\n    public void setSize(Size size) {\n        setState(() -> {\n            this.width = size.width();\n            this.height = size.height();\n        });\n    }\n\n    public Size toSize() {\n        return Size.of(this.width, this.height);\n    }\n\n    public void transformToParent(Matrix3x2f mat) {\n        mat.translate((float) this.x, (float) this.y);\n    }\n\n    public void transformToParent(Matrix3x2fStack matrices) {\n        matrices.translate((float) this.x, (float) this.y);\n    }\n\n    public void transformToWidget(Matrix3x2f mat) {\n        mat.translate((float) -this.x, (float) -this.y);\n    }\n\n    public void transformToWidget(Matrix3x2fStack matrices) {\n        matrices.translate((float) -this.x, (float) -this.y);\n    }\n\n    public void toParentCoordinates(Vector2d vec) {\n        vec.add(this.x, this.y);\n    }\n\n    public void toWidgetCoordinates(Vector2d vec) {\n        vec.sub(this.x, this.y);\n    }\n\n    public void setExtent(LayoutAxis axis, double value) {\n        switch (axis) {\n            case HORIZONTAL -> setWidth(value);\n            case VERTICAL -> setHeight(value);\n        }\n    }\n\n    public double getExtent(LayoutAxis axis) {\n        return switch (axis) {\n            case HORIZONTAL -> width();\n            case VERTICAL -> height();\n        };\n    }\n\n    public void setCoordinate(LayoutAxis axis, double value) {\n        switch (axis) {\n            case HORIZONTAL -> setX(value);\n            case VERTICAL -> setY(value);\n        }\n    }\n\n    public double getCoordinate(LayoutAxis axis) {\n        return switch (axis) {\n            case HORIZONTAL -> x();\n            case VERTICAL -> y();\n        };\n    }\n\n    protected void setState(Runnable action) {\n        action.run();\n        this.recompute();\n    }\n\n    public void recompute() {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/BuildScope.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport io.wispforest.owo.Owo;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\n\npublic class BuildScope {\n    private final List<WidgetProxy> dirtyProxies = new ArrayList<>();\n    private boolean resortProxies = true;\n\n    private final @Nullable Runnable scheduleRebuild;\n\n    public BuildScope(@Nullable Runnable scheduleRebuild) {\n        this.scheduleRebuild = scheduleRebuild;\n    }\n\n    public BuildScope() {\n        this(null);\n    }\n\n    // ---\n\n    public void scheduleRebuild(WidgetProxy proxy) {\n        this.dirtyProxies.add(proxy);\n        this.resortProxies = true;\n\n        if (this.scheduleRebuild != null) {\n            this.scheduleRebuild.run();\n        }\n    }\n\n    public boolean rebuildDirtyProxies() {\n        if (this.dirtyProxies.isEmpty()) return false;\n\n        this.dirtyProxies.sort(Comparator.naturalOrder());\n\n        for (var idx = 0; idx < this.dirtyProxies.size(); idx = this.nextDirtyIndex(idx)) {\n            this.dirtyProxies.get(idx).rebuild();\n        }\n\n        if (Owo.DEBUG && this.dirtyProxies.stream().anyMatch(BuildScope::isMissed)) {\n            throw new IllegalStateException(\n                \"missed the following dirty proxies: [\"\n                    + this.dirtyProxies.stream().filter(BuildScope::isMissed).map(Objects::toString).collect(Collectors.joining(\", \"))\n                    + \"]\"\n            );\n        }\n\n        this.dirtyProxies.clear();\n        return true;\n    }\n\n    private int nextDirtyIndex(int idx) {\n        if (!this.resortProxies) return idx + 1;\n\n        this.dirtyProxies.sort(Comparator.naturalOrder());\n        this.resortProxies = false;\n\n        idx++;\n        while (idx > 0 && this.dirtyProxies.get(idx - 1).needsRebuild()) {\n            idx--;\n        }\n\n        return idx;\n    }\n\n    // ---\n\n    private static boolean isMissed(WidgetProxy proxy) {\n        return proxy.needsRebuild && proxy.lifecycle == WidgetProxy.Lifecycle.LIVE;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/ComposedProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic abstract non-sealed class ComposedProxy extends WidgetProxy {\n\n    protected @Nullable WidgetProxy child;\n\n    public ComposedProxy(Widget widget) {\n        super(widget);\n    }\n\n    public WidgetProxy child() {\n        return this.child;\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {\n        if (this.child != null) visitor.visit(this.child);\n    }\n\n    // ---\n\n    private WidgetInstance<?> descendantInstance;\n\n    @Override\n    public @Nullable WidgetInstance<?> instance() {\n        return this.descendantInstance;\n    }\n\n    @Override\n    public void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot) {\n        this.descendantInstance = instance;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/InheritedProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\n\npublic class InheritedProxy extends ComposedProxy {\n\n    private final List<WidgetProxy> dependents = new ArrayList<>();\n\n    public InheritedProxy(InheritedWidget widget) {\n        super(widget);\n    }\n\n    public void addDependency(WidgetProxy dependent, @Nullable Object dependency) {\n        this.dependents.add(dependent);\n    }\n\n    public void removeDependent(WidgetProxy dependent) {\n        this.dependents.remove(dependent);\n    }\n\n    protected boolean mustRebuildDependent(WidgetProxy dependent) {\n        return true;\n    }\n\n    public void notifyDependent(WidgetProxy dependent) {\n        dependent.notifyDependenciesChanged();\n    }\n\n    @Override\n    public void mount(WidgetProxy parent, @Nullable Object slot) {\n        super.mount(parent, slot);\n        this.inheritedProxies = this.inheritedProxies != null ? new HashMap<>(this.inheritedProxies) : new HashMap<>();\n        this.inheritedProxies.put(((InheritedWidget) this.widget()).inheritedKey(), this);\n\n        this.rebuild();\n    }\n\n    @Override\n    public void updateWidget(Widget newWidget) {\n        var shouldUpdate = ((InheritedWidget) this.widget()).mustRebuildDependents((InheritedWidget) newWidget);\n\n        super.updateWidget(newWidget);\n\n        this.rebuild(true);\n        if (shouldUpdate) {\n            for (var dependent : this.dependents) {\n                if (!this.mustRebuildDependent(dependent)) continue;\n                this.notifyDependent(dependent);\n            }\n        }\n    }\n\n    @Override\n    protected void doRebuild() {\n        super.doRebuild();\n        this.child = this.refreshChild(this.child, ((InheritedWidget) this.widget()).child, this.slot());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/InstanceWidgetProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic abstract non-sealed class InstanceWidgetProxy extends WidgetProxy {\n\n    protected final WidgetInstance<InstanceWidget> instance;\n\n    private final List<WidgetProxy> ancestorsUntilNextInstanceProxy = new ArrayList<>();\n\n    protected InstanceWidgetProxy(InstanceWidget widget) {\n        super(widget);\n\n        //noinspection unchecked\n        this.instance = (WidgetInstance<InstanceWidget>) widget.instantiate();\n        Preconditions.checkNotNull(this.instance, \"Widget#instantiate must return a non-null instance\");\n    }\n\n    @Override\n    public WidgetInstance<? extends InstanceWidget> instance() {\n        return this.instance;\n    }\n\n    @Override\n    public void mount(WidgetProxy parent, @Nullable Object slot) {\n        super.mount(parent, slot);\n\n        var ancestor = parent;\n        while (!(ancestor instanceof InstanceWidgetProxy)) {\n            this.ancestorsUntilNextInstanceProxy.add(ancestor);\n            ancestor = ancestor.parent();\n        }\n\n        this.ancestorsUntilNextInstanceProxy.add(ancestor);\n\n        this.rebuild();\n        this.notifyAncestors();\n    }\n\n    @Override\n    public void updateSlot(@Nullable Object newSlot) {\n        super.updateSlot(newSlot);\n        this.notifyAncestors();\n    }\n\n    @Override\n    public void unmount() {\n        super.unmount();\n        this.instance.dispose();\n        this.ancestorsUntilNextInstanceProxy.clear();\n    }\n\n    @Override\n    public void updateWidget(Widget newWidget) {\n        super.updateWidget(newWidget);\n        this.instance.setWidget((InstanceWidget) newWidget);\n    }\n\n    private void notifyAncestors() {\n        for (var listener : this.ancestorsUntilNextInstanceProxy) {\n            listener.notifyDescendantInstance(this.instance, this.slot());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/LeafInstanceWidgetProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class LeafInstanceWidgetProxy extends InstanceWidgetProxy {\n    public LeafInstanceWidgetProxy(LeafInstanceWidget widget) {\n        super(widget);\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {}\n\n    @Override\n    public void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot) {\n        Preconditions.checkState(false, \"a leaf proxy cannot have descendant instances\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/MultiChildInstanceWidgetProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Key;\nimport io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class MultiChildInstanceWidgetProxy extends InstanceWidgetProxy {\n    public List<WidgetProxy> children = new ArrayList<>();\n    public List<WidgetInstance<?>> childInstances = new ArrayList<>();\n\n    public MultiChildInstanceWidgetProxy(MultiChildInstanceWidget widget) {\n        super(widget);\n    }\n\n    @Override\n    public MultiChildWidgetInstance<? extends InstanceWidget> instance() {\n        //noinspection unchecked\n        return (MultiChildWidgetInstance<? extends InstanceWidget>) super.instance();\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {\n        for (var child : children) {\n            visitor.visit(child);\n        }\n    }\n\n    @Override\n    public void updateWidget(Widget newWidget) {\n        super.updateWidget(newWidget);\n        rebuild(true);\n    }\n\n    @Override\n    public void doRebuild() {\n        super.doRebuild();\n        var newWidgets = ((MultiChildInstanceWidget) this.widget()).children;\n\n        var newChildrenTop = 0;\n        var oldChildrenTop = 0;\n        var newChildrenBottom = newWidgets.size() - 1;\n        var oldChildrenBottom = this.children.size() - 1;\n\n        var newChildren = Stream.<WidgetProxy>generate(() -> null).limit(newWidgets.size()).collect(Collectors.toList());\n\n        // we already set up the new child instance list, so that any\n        // notifyDescendantInstance invocations caused by the below\n        // refreshChild calls always index into the correct list\n        this.childInstances = Stream.<WidgetInstance<?>>generate(() -> null).limit(newChildren.size()).collect(Collectors.toList());\n        copyInto(this.childInstances, 0, this.instance().children, 0, Math.min(this.childInstances.size(), this.instance().children.size()));\n\n        if (this.instance().children.size() > this.childInstances.size()) {\n            this.instance().markNeedsLayout();\n        }\n        this.instance().children = this.childInstances;\n\n        // sync from the top\n        while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {\n            var oldChild = this.children.get(oldChildrenTop);\n            var newWidget = newWidgets.get(newChildrenTop);\n\n            if (!Widget.canUpdate(oldChild.widget(), newWidget)) {\n                break;\n            }\n\n            newChildren.set(newChildrenTop, this.refreshChild(oldChild, newWidget, newChildrenTop));\n            Preconditions.checkNotNull(this.childInstances.get(newChildrenTop));\n\n            oldChildrenTop++;\n            newChildrenTop++;\n        }\n\n        // scan from the bottom\n        while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {\n            var oldChild = this.children.get(oldChildrenTop);\n            var newWidget = newWidgets.get(newChildrenTop);\n\n            if (!Widget.canUpdate(oldChild.widget(), newWidget)) {\n                break;\n            }\n\n            oldChildrenTop++;\n            newChildrenTop++;\n        }\n\n        // scan middle, store keyed and disposed un-keyed\n\n        var hasOldChildren = oldChildrenTop <= oldChildrenBottom;\n        Map<Key, WidgetProxy> keyedOldChildren = null;\n\n        if (hasOldChildren) {\n            keyedOldChildren = new HashMap<>();\n            while (oldChildrenTop <= oldChildrenBottom) {\n                var oldChild = this.children.get(oldChildrenTop);\n                var key = oldChild.widget().key();\n\n                if (key != null) {\n                    keyedOldChildren.put(key, oldChild);\n                } else {\n                    oldChild.unmount();\n                }\n\n                oldChildrenTop++;\n            }\n        }\n\n        // sync middle, updating keyed\n\n        while (newChildrenTop <= newChildrenBottom) {\n            WidgetProxy oldChild = null;\n            var newWidget = newWidgets.get(newChildrenTop);\n\n            if (hasOldChildren) {\n                var key = newWidget.key();\n                if (key != null) {\n                    oldChild = keyedOldChildren.get(key);\n                    if (oldChild != null) {\n                        if (Widget.canUpdate(oldChild.widget(), newWidget)) {\n                            keyedOldChildren.remove(key);\n                        } else {\n                            oldChild = null;\n                        }\n                    }\n                }\n            }\n\n            newChildren.set(newChildrenTop, this.refreshChild(oldChild, newWidget, newChildrenTop));\n            Preconditions.checkNotNull(this.childInstances.get(newChildrenTop));\n\n            newChildrenTop++;\n        }\n\n        newChildrenBottom = newWidgets.size() - 1;\n        oldChildrenBottom = this.children.size() - 1;\n\n        while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {\n            var oldChild = this.children.get(oldChildrenTop);\n            var newWidget = newWidgets.get(newChildrenTop);\n\n            newChildren.set(newChildrenTop, this.refreshChild(oldChild, newWidget, newChildrenTop));\n            Preconditions.checkNotNull(this.childInstances.get(newChildrenTop));\n\n            oldChildrenTop++;\n            newChildrenTop++;\n        }\n\n        // dispose keyed proxies that were not reused\n        if (hasOldChildren && !keyedOldChildren.isEmpty()) {\n            for (var proxy : keyedOldChildren.values()) {\n                proxy.unmount();\n            }\n        }\n\n        // finally, install new children\n        this.children = newChildren;\n    }\n\n    @Override\n    public void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot) {\n        this.instance().insertChild(((Integer) slot).intValue(), instance);\n    }\n\n    @SuppressWarnings(\"SameParameterValue\")\n    private static <T> void copyInto(List<T> target, int at, List<T> source, int from, int to) {\n        var copyCount = to - from;\n        for (var i = 0; i < copyCount; i++) {\n            target.set(at + i, source.get(from + i));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/OptionalChildInstanceWidgetProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.OptionalChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class OptionalChildInstanceWidgetProxy extends InstanceWidgetProxy {\n\n    protected @Nullable WidgetProxy child;\n\n    public OptionalChildInstanceWidgetProxy(OptionalChildInstanceWidget widget) {\n        super(widget);\n    }\n\n    @Override\n    public OptionalChildWidgetInstance<? extends InstanceWidget> instance() {\n        return (OptionalChildWidgetInstance<? extends InstanceWidget>) super.instance();\n    }\n\n    @Override\n    public void updateWidget(Widget newWidget) {\n        super.updateWidget(newWidget);\n        this.rebuild(true);\n    }\n\n    @Override\n    protected void doRebuild() {\n        super.doRebuild();\n        this.child = this.refreshChild(this.child, ((OptionalChildInstanceWidget) this.widget()).child, null);\n\n        if (((OptionalChildInstanceWidget) this.widget()).child == null) {\n            this.instance().setChild(null);\n        }\n    }\n\n    @Override\n    public void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot) {\n        this.instance().setChild(instance);\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {\n        if (this.child != null) {\n            visitor.visit(this.child);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/ProxyHost.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport net.minecraft.client.Minecraft;\n\nimport java.time.Duration;\n\npublic interface ProxyHost {\n\n    Minecraft client();\n\n    void scheduleAnimationCallback(AnimationCallback callback);\n\n    long scheduleDelayedCallback(Duration delay, Runnable callback);\n\n    void cancelDelayedCallback(long id);\n\n    void schedulePostLayoutCallback(Runnable callback);\n\n    interface AnimationCallback {\n        void run(Duration delta);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/SingleChildInstanceWidgetProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class SingleChildInstanceWidgetProxy extends InstanceWidgetProxy {\n    protected WidgetProxy child;\n\n    public SingleChildInstanceWidgetProxy(SingleChildInstanceWidget widget) {\n        super(widget);\n    }\n\n    @Override\n    public SingleChildWidgetInstance<? extends InstanceWidget> instance() {\n        return (SingleChildWidgetInstance<? extends InstanceWidget>) super.instance();\n    }\n\n    @Override\n    public void updateWidget(Widget newWidget) {\n        super.updateWidget(newWidget);\n        this.rebuild(true);\n    }\n\n    @Override\n    protected void doRebuild() {\n        super.doRebuild();\n        this.child = this.refreshChild(this.child, ((SingleChildInstanceWidget) this.widget()).child, null);\n    }\n\n    @Override\n    public void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot) {\n        this.instance().setChild(instance);\n    }\n\n    @Override\n    public void visitChildren(Visitor visitor) {\n        if (this.child != null) {\n            visitor.visit(this.child);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/StatefulProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class StatefulProxy extends ComposedProxy {\n\n    private final WidgetState<StatefulWidget> state;\n    private boolean dependenciesChanged = false;\n\n    public StatefulProxy(StatefulWidget widget) {\n        super(widget);\n\n        //noinspection unchecked\n        this.state = (WidgetState<StatefulWidget>) widget.createState();\n        this.state.widget = (StatefulWidget) this.widget();\n        this.state.owner = this;\n    }\n\n    public WidgetState<?> state() {\n        return this.state;\n    }\n\n    @Override\n    public void mount(WidgetProxy parent, @Nullable Object slot) {\n        super.mount(parent, slot);\n\n        this.state.init();\n        this.rebuild();\n    }\n\n    @Override\n    public void notifyDependenciesChanged() {\n        super.notifyDependenciesChanged();\n        this.dependenciesChanged = true;\n    }\n\n    @Override\n    public void unmount() {\n        super.unmount();\n        this.state.dispose();\n    }\n\n    @Override\n    public void updateWidget(Widget newWidget) {\n        super.updateWidget(newWidget);\n\n        var oldWidget = this.state.widget;\n        this.state.widget = (StatefulWidget) newWidget;\n        this.state.didUpdateWidget(oldWidget);\n\n        this.rebuild(true);\n    }\n\n    @Override\n    protected void doRebuild() {\n        if (this.dependenciesChanged) {\n            this.state.notifyDependenciesChanged();\n            this.dependenciesChanged = false;\n        }\n\n        var newWidget = this.state.build(this);\n        super.doRebuild();\n\n        this.child = this.refreshChild(this.child, newWidget, this.slot());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/StatelessProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class StatelessProxy extends ComposedProxy {\n    public StatelessProxy(StatelessWidget widget) {\n        super(widget);\n    }\n\n    @Override\n    public void mount(WidgetProxy parent, @Nullable Object slot) {\n        super.mount(parent, slot);\n        this.rebuild();\n    }\n\n    @Override\n    public void updateWidget(Widget newWidget) {\n        super.updateWidget(newWidget);\n        this.rebuild(true);\n    }\n\n    @Override\n    protected void doRebuild() {\n        var newWidget = ((StatelessWidget) this.widget()).build(this);\n        super.doRebuild();\n\n        this.child = this.refreshChild(this.child, newWidget, this.slot());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/WidgetProxy.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.MustBeInvokedByOverriders;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\npublic abstract sealed class WidgetProxy implements BuildContext, Comparable<WidgetProxy> permits ComposedProxy, InstanceWidgetProxy {\n\n    private Widget widget;\n\n    private @Nullable WidgetProxy parent;\n    private BuildScope parentBuildScope;\n\n    private int depth = -1;\n    private @Nullable ProxyHost host;\n    private @Nullable Object slot;\n    public Lifecycle lifecycle = Lifecycle.INITIAL;\n    protected boolean needsRebuild = true;\n\n    protected Map<Object, InheritedProxy> inheritedProxies = null;\n    protected Set<InheritedProxy> dependencies = null;\n\n    public WidgetProxy(Widget widget) {\n        this.widget = widget;\n        this.widget.freeze();\n    }\n\n    public void mount(WidgetProxy parent, @Nullable Object slot) {\n        Preconditions.checkArgument(parent.mounted(), \"parent proxy must be mounted before its children\");\n\n        Preconditions.checkState(this.lifecycle == Lifecycle.INITIAL, \"proxy must be in INITIAL lifecycle state when mount() is called\");\n        this.lifecycle = Lifecycle.LIVE;\n\n        this.inheritedProxies = parent.inheritedProxies;\n\n        this.parent = parent;\n        this.parentBuildScope = parent.buildScope();\n        this.setDepth(parent.depth + 1);\n        this.slot = slot;\n        this.host = parent.host;\n    }\n\n    @MustBeInvokedByOverriders\n    public void updateSlot(@Nullable Object newSlot) {\n        this.slot = newSlot;\n    }\n\n    public void unmount() {\n        Preconditions.checkState(this.lifecycle == Lifecycle.LIVE, \"proxy must be in LIVE lifecycle state when unmount() is called\");\n        this.lifecycle = Lifecycle.DEAD;\n\n        if (this.dependencies != null) {\n            for (var dependency : this.dependencies) {\n                if (dependency != null) dependency.removeDependent(this);\n            }\n        }\n\n        visitChildren(Visitors.UNMOUNT);\n    }\n\n    public void markNeedsRebuild() {\n        if (this.needsRebuild) return;\n\n        this.needsRebuild = true;\n        this.buildScope().scheduleRebuild(this);\n    }\n\n    public void reassemble() {\n        this.markNeedsRebuild();\n        this.visitChildren(Visitors.REASSEMBLE);\n    }\n\n    // ---\n\n    protected @Nullable WidgetProxy refreshChild(@Nullable WidgetProxy child, @Nullable Widget newWidget, @Nullable Object newSlot) {\n        if (newWidget == null) {\n            if (child != null) child.unmount();\n            return null;\n        }\n\n        if (child != null && Widget.canUpdate(child.widget, newWidget)) {\n            if (!Objects.equals(child.slot, newSlot)) {\n                child.updateSlot(newSlot);\n            }\n\n            if (child.widget != newWidget) {\n                child.updateWidget(newWidget);\n            }\n\n            return child;\n        } else {\n            if (child != null) {\n                child.unmount();\n            }\n\n            var newProxy = newWidget.proxy();\n            newProxy.mount(this, newSlot);\n            return newProxy;\n        }\n    }\n\n    @MustBeInvokedByOverriders\n    public void updateWidget(Widget newWidget) {\n        this.widget = newWidget;\n        this.widget.freeze();\n    }\n\n    public final void rebuild() {\n        this.rebuild(false);\n    }\n\n    public final void rebuild(boolean force) {\n        if (!(force || (this.needsRebuild && this.lifecycle == Lifecycle.LIVE))) return;\n\n        this.doRebuild();\n    }\n\n    @MustBeInvokedByOverriders\n    protected void doRebuild() {\n        this.needsRebuild = false;\n    }\n\n    // ---\n\n    @Override\n    public <T> @Nullable T getAncestor(Class<T> ancestorClass, Object inheritedKey) {\n        var ancestor = this.inheritedProxies != null ? this.inheritedProxies.get(inheritedKey) : null;\n\n        if (ancestor != null) {\n            Preconditions.checkArgument(ancestorClass == ancestor.widget().getClass(), \"attempted to look up an ancestor using an inheritedKey pointing to one of a different type\");\n\n            //noinspection unchecked\n            return (T) ancestor.widget();\n        }\n\n        return null;\n    }\n\n    @Override\n    public <T> @Nullable T dependOnAncestor(Class<T> ancestorClass, Object inheritedKey, @Nullable Object dependency) {\n        var ancestor = this.inheritedProxies != null ? this.inheritedProxies.get(inheritedKey) : null;\n        if (ancestor != null) {\n            Preconditions.checkArgument(ancestorClass == ancestor.widget().getClass(), \"attempted to look up an ancestor using an inheritedKey pointing to one of a different type\");\n\n            if (this.dependencies == null) {\n                this.dependencies = new HashSet<>();\n            }\n\n            ancestor.addDependency(this, dependency);\n            this.dependencies.add(ancestor);\n\n            //noinspection unchecked\n            return (T) ancestor.widget();\n        }\n\n        return null;\n    }\n\n    public void notifyDependenciesChanged() {\n        this.markNeedsRebuild();\n    }\n\n    // ---\n\n    public abstract void visitChildren(Visitor visitor);\n\n    @Override\n    public abstract @Nullable WidgetInstance<?> instance();\n\n    public abstract void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot);\n\n    // ---\n\n    public Widget widget() {\n        return this.widget;\n    }\n\n    public @Nullable WidgetProxy parent() {\n        return this.parent;\n    }\n\n    public boolean mounted() {\n        return this.parent != null;\n    }\n\n    public BuildScope buildScope() {\n        Preconditions.checkNotNull(this.parentBuildScope, \"parent build scope not set\");\n        return this.parentBuildScope;\n    }\n\n    public @Nullable Object slot() {\n        return this.slot;\n    }\n\n    public ProxyHost host() {\n        return this.host;\n    }\n\n    public boolean needsRebuild() {\n        return this.needsRebuild;\n    }\n\n    public int depth() {\n        return this.depth;\n    }\n\n    public void setDepth(int depth) {\n        if (this.depth == depth) return;\n\n        this.depth = depth;\n        this.visitChildren(child -> child.setDepth(this.depth + 1));\n    }\n\n    // ---\n\n    /// Set the host of this proxy, reserved for use by\n    /// root proxy implementations. In all other scenarios,\n    /// the host is to be taken from the parent in [#mount]\n    protected void rootSetHost(ProxyHost host) {\n        this.host = host;\n    }\n\n    // ---\n\n    @Override\n    public int compareTo(@NotNull WidgetProxy o) {\n        return Integer.compare(this.depth, o.depth);\n    }\n\n    // ---\n\n    @FunctionalInterface\n    public interface Visitor {\n        void visit(WidgetProxy child);\n    }\n\n    public enum Lifecycle {\n        INITIAL, LIVE, DEAD\n    }\n}\n\nenum Visitors implements WidgetProxy.Visitor {\n    UNMOUNT(WidgetProxy::unmount),\n    REASSEMBLE(WidgetProxy::reassemble);\n\n    private final WidgetProxy.Visitor delegate;\n\n    Visitors(WidgetProxy.Visitor delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public void visit(WidgetProxy child) {\n        this.delegate.visit(child);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/proxy/WidgetState.java",
    "content": "package io.wispforest.owo.braid.framework.proxy;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\n\npublic abstract class WidgetState<T extends StatefulWidget> {\n\n    StatefulProxy owner;\n    @Nullable T widget;\n\n    public abstract Widget build(BuildContext context);\n\n    public BuildContext context() {\n        if (Owo.DEBUG) {\n            Preconditions.checkNotNull(this.owner, \"cannot access this.context() on a WidgetState before init() is called\");\n        }\n\n        return this.owner;\n    }\n\n    public void init() {}\n    public void dispose() {}\n\n    public void didUpdateWidget(T oldWidget) {}\n    public void notifyDependenciesChanged() {}\n\n    public final void setState(Runnable fn) {\n        Preconditions.checkState(this.owner != null, \"setState invoked on WidgetState before it was mounted\");\n\n        fn.run();\n        this.owner.markNeedsRebuild();\n    }\n\n    public final long scheduleDelayedCallback(Duration after, Runnable callback) {\n        return this.owner.host().scheduleDelayedCallback(after, callback);\n    }\n\n    public final void cancelDelayedCallback(long id) {\n        this.owner.host().cancelDelayedCallback(id);\n    }\n\n    public final void scheduleAnimationCallback(ProxyHost.AnimationCallback callback) {\n        this.owner.host().scheduleAnimationCallback(callback);\n    }\n\n    public final void schedulePostLayoutCallback(Runnable callback) {\n        this.owner.host().schedulePostLayoutCallback(callback);\n    }\n\n    public T widget() {\n        Preconditions.checkNotNull(this.widget, \"widget() accessor on a WidgetState was used before init()\");\n        return this.widget;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/InheritedWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.proxy.InheritedProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\n\npublic abstract class InheritedWidget extends Widget {\n    public final Widget child;\n\n    protected InheritedWidget(Widget child) {\n        this.child = child;\n    }\n\n    @Override\n    public WidgetProxy proxy() {\n        return new InheritedProxy(this);\n    }\n\n    // ---\n\n    public Object inheritedKey() {\n        return this.getClass();\n    }\n\n    public abstract boolean mustRebuildDependents(InheritedWidget newWidget);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/InstanceWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\n\npublic abstract class InstanceWidget extends Widget {\n    public abstract WidgetInstance<?> instantiate();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/Key.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport com.google.common.base.Preconditions;\nimport org.jetbrains.annotations.NotNull;\n\npublic class Key {\n\n    final String value;\n\n    private Key(String value) {\n        this.value = value;\n    }\n\n    public static Key of(@NotNull String value) {\n        Preconditions.checkNotNull(value ,\"the value of a key must never be null\");\n        return new Key(value);\n    }\n\n    // ---\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Key key = (Key) o;\n        return this.value.equals(key.value);\n    }\n\n    @Override\n    public int hashCode() {\n        return this.value.hashCode();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/LeafInstanceWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.LeafInstanceWidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\n\npublic abstract class LeafInstanceWidget extends InstanceWidget {\n\n    @Override\n    public abstract LeafWidgetInstance<?> instantiate();\n\n    @Override\n    public WidgetProxy proxy() {\n        return new LeafInstanceWidgetProxy(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/MultiChildInstanceWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.MultiChildInstanceWidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\n\nimport java.util.List;\n\npublic abstract class MultiChildInstanceWidget extends InstanceWidget {\n    public final List<? extends Widget> children;\n\n    protected MultiChildInstanceWidget(List<? extends Widget> children) {\n        this.children = children;\n    }\n\n    @Override\n    public abstract MultiChildWidgetInstance<?> instantiate();\n\n    @Override\n    public WidgetProxy proxy() {\n        return new MultiChildInstanceWidgetProxy(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/OptionalChildInstanceWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.OptionalChildInstanceWidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport org.jetbrains.annotations.Nullable;\n\npublic abstract class OptionalChildInstanceWidget extends InstanceWidget {\n    public final @Nullable Widget child;\n\n    public OptionalChildInstanceWidget(@Nullable Widget child) {\n        this.child = child;\n    }\n\n    @Override\n    public abstract OptionalChildWidgetInstance<?> instantiate();\n\n    @Override\n    public WidgetProxy proxy() {\n        return new OptionalChildInstanceWidgetProxy(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/SingleChildInstanceWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.SingleChildInstanceWidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\n\npublic abstract class SingleChildInstanceWidget extends InstanceWidget {\n\n    public final Widget child;\n\n    protected SingleChildInstanceWidget(Widget child) {\n        this.child = child;\n    }\n\n    @Override\n    public abstract SingleChildWidgetInstance<?> instantiate();\n\n    @Override\n    public WidgetProxy proxy() {\n        return new SingleChildInstanceWidgetProxy(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/StatefulWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.proxy.StatefulProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\n\npublic abstract class StatefulWidget extends Widget {\n    public abstract WidgetState<?> createState();\n\n    @Override\n    public WidgetProxy proxy() {\n        return new StatefulProxy(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/StatelessWidget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.StatelessProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\n\npublic abstract class StatelessWidget extends Widget {\n\n    public abstract Widget build(BuildContext context);\n\n    @Override\n    public WidgetProxy proxy() {\n        return new StatelessProxy(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/Widget.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\n\npublic abstract class Widget {\n    private boolean mutable = true;\n\n    @ApiStatus.Internal\n    public final void freeze() {\n        mutable = false;\n    }\n\n    protected final void assertMutable() {\n        if (!this.mutable) throw new ImmutableWidgetError();\n    }\n\n    // ---\n\n    private @Nullable Key key;\n\n    public Widget key(Key key) {\n        this.assertMutable();\n        this.key = key;\n\n        return this;\n    }\n\n    public @Nullable Key key() {\n        return this.key;\n    }\n\n    // ---\n\n    public abstract WidgetProxy proxy();\n\n    public static boolean canUpdate(Widget oldWidget, Widget newWidget) {\n        return oldWidget.getClass() == newWidget.getClass() && Objects.equals(oldWidget.key, newWidget.key);\n    }\n\n    public static <T extends Widget> WidgetSetupCallback<T> noSetup() {\n        //noinspection unchecked\n        return NO_SETUP;\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    private static final WidgetSetupCallback NO_SETUP = widget -> {};\n}\n\nclass ImmutableWidgetError extends Error {\n    public ImmutableWidgetError() {\n        // TODO: more detailed explanation of why this is bad\n        super(\"A mutation on a widget was attempted after the widget was frozen\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/framework/widget/WidgetSetupCallback.java",
    "content": "package io.wispforest.owo.braid.framework.widget;\n\npublic interface WidgetSetupCallback<T extends Widget> {\n    void setup(T widget);\n\n    default WidgetSetupCallback<T> compose(WidgetSetupCallback<? super T> before) {\n        return widget -> {\n            before.setup(widget);\n            this.setup(widget);\n        };\n    }\n\n    default WidgetSetupCallback<T> andThen(WidgetSetupCallback<? super T> after) {\n        return widget -> {\n            this.setup(widget);\n            after.setup(widget);\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/BraidGuiRenderer.java",
    "content": "package io.wispforest.owo.braid.util;\n\nimport com.mojang.blaze3d.buffers.GpuBufferSlice;\nimport com.mojang.blaze3d.pipeline.RenderTarget;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Surface;\nimport io.wispforest.owo.mixin.braid.GameRendererAccessor;\nimport io.wispforest.owo.mixin.braid.GuiRendererAccessor;\nimport io.wispforest.owo.util.pond.BraidGuiRendererExtension;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.render.GuiRenderer;\nimport net.minecraft.client.gui.render.state.GuiRenderState;\nimport net.minecraft.client.renderer.fog.FogRenderer;\n\nimport java.util.ArrayList;\n\npublic class BraidGuiRenderer extends GuiRenderer {\n\n    private final Minecraft client;\n\n    public BraidGuiRenderer(Minecraft client) {\n        super(\n            new GuiRenderState(),\n            client.renderBuffers().bufferSource(),\n            client.gameRenderer.getSubmitNodeStorage(),\n            client.gameRenderer.getFeatureRenderDispatcher(),\n            new ArrayList<>(((GuiRendererAccessor) ((GameRendererAccessor) client.gameRenderer).owo$getGuiRenderer()).owo$getPictureInPictureRenderers().values())\n        );\n        this.client = client;\n    }\n\n    public GuiGraphics newGraphics(double mouseX, double mouseY) {\n        this.trySetFabricState();\n        return new GuiGraphics(\n            this.client,\n            ((GuiRendererAccessor) this).owo$getRenderState(),\n            (int) mouseX, (int) mouseY\n        );\n    }\n\n    private boolean fabricStateSet = false;\n    private void trySetFabricState() {\n        if (this.fabricStateSet) {\n            return;\n        }\n\n        try {\n            var initField = GuiRenderer.class.getDeclaredField(\"hasFabricInitialized\");\n            initField.setAccessible(true);\n            initField.set(this, true);\n\n            var commandQueueField = GuiRenderer.class.getDeclaredField(\"orderedRenderCommandQueue\");\n            commandQueueField.setAccessible(true);\n            commandQueueField.set(this, this.client.gameRenderer.getSubmitNodeStorage());\n        } catch (IllegalAccessException | NoSuchFieldException e) {\n            Owo.LOGGER.warn(\"Failed to apply braid's Fabric API GuiRendererMixin workaround, there might be crashes with texture and window surfaces\");\n        } finally {\n            this.fabricStateSet = true;\n        }\n    }\n\n    public void render(Target target) {\n        ((BraidGuiRendererExtension) this).owo$setTarget(target);\n        this.render(((GameRendererAccessor) this.client.gameRenderer).owo$getFogRenderer().getBuffer(FogRenderer.FogMode.NONE));\n    }\n\n    @Override\n    @Deprecated\n    public void render(GpuBufferSlice fogBuffer) {\n        super.render(fogBuffer);\n    }\n\n    public record Target(RenderTarget framebuffer, Surface surface) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/BraidHudElement.java",
    "content": "package io.wispforest.owo.braid.util;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.EventBinding;\nimport io.wispforest.owo.braid.core.Surface;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;\nimport net.fabricmc.fabric.api.client.rendering.v1.hud.HudElement;\nimport net.minecraft.client.DeltaTracker;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport org.jetbrains.annotations.Nullable;\n\npublic class BraidHudElement implements HudElement {\n\n    public final Widget widget;\n    private AppState app;\n\n    public BraidHudElement(Widget widget) {\n        this.widget = widget;\n\n        ClientPlayConnectionEvents.JOIN.register((clientPlayNetworkHandler, packetSender, minecraftClient) -> {\n            this.setupAppState();\n        });\n\n        ClientPlayConnectionEvents.DISCONNECT.register((clientPlayNetworkHandler, minecraftClient) -> {\n            this.resetAppState();\n        });\n    }\n\n    public @Nullable AppState app() {\n        return this.app;\n    }\n\n    @Override\n    public void render(GuiGraphics graphics, DeltaTracker deltaTracker) {\n        if (this.app == null) {\n            if (!Owo.DEBUG) {\n                return;\n            }\n\n            throw new IllegalStateException(\"tried to render a BraidHudElement before it was initialized\");\n        }\n\n        this.app.processEvents(deltaTracker.getGameTimeDeltaTicks());\n        this.app.draw(graphics);\n    }\n\n    protected void setupAppState() {\n        this.app = new AppState(\n            null,\n            AppState.formatName(\"BraidHudElement\", widget),\n            Minecraft.getInstance(),\n            new Surface.Default(),\n            new EventBinding.Headless(),\n            widget\n        );\n    }\n\n    protected void resetAppState() {\n        this.app.dispose();\n        this.app = null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/BraidToast.java",
    "content": "package io.wispforest.owo.braid.util;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.EventBinding;\nimport io.wispforest.owo.braid.core.Surface;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Align;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.toasts.Toast;\nimport net.minecraft.client.gui.components.toasts.ToastManager;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\n\npublic class BraidToast implements Toast {\n\n    private final @Nullable Duration timeout;\n    private final Object token;\n    private final AppState app;\n\n    private EmbedderRoot.Instance rootInstance;\n\n    private BraidToast(@Nullable Duration timeout, @Nullable Object token, Widget widget) {\n        this.timeout = timeout;\n        this.token = token != null ? token : new Object();\n        this.app = new AppState(\n            Owo.LOGGER,\n            AppState.formatName(\"BraidToast\", widget),\n            Minecraft.getInstance(),\n            new Surface.Default(),\n            new EventBinding.Headless(),\n            new Align(\n                Alignment.TOP_LEFT,\n                new EmbedderRoot(\n                    instance -> this.rootInstance = instance,\n                    new BraidToastProvider(\n                        this,\n                        widget\n                    )\n                )\n            )\n        );\n\n        this.app.processEvents(0);\n    }\n\n    public static void show(@Nullable Duration timeout, @Nullable Object token, Widget widget) {\n        Minecraft.getInstance().getToastManager().addToast(new BraidToast(timeout, token, widget));\n    }\n\n    public static void hideWithToken(Object token) {\n        var toast = Minecraft.getInstance().getToastManager().getToast(BraidToast.class, token);\n        if (toast != null) {\n            toast.visibility = Visibility.HIDE;\n        }\n    }\n\n    public static void hide(BuildContext context) {\n        var provider = context.getAncestor(BraidToastProvider.class);\n        Preconditions.checkNotNull(provider, \"BraidToast.hide can only be used from inside a BraidToast's widget tree\");\n\n        provider.toast.visibility = Visibility.HIDE;\n    }\n\n    // ---\n\n    @ApiStatus.Internal\n    public void dispose() {\n        this.app.dispose();\n    }\n\n    @Override\n    public void render(GuiGraphics graphics, Font font, long startTime) {\n        this.app.draw(graphics);\n    }\n\n    @Override\n    public int width() {\n        return (int) this.rootInstance.transform.width();\n    }\n\n    @Override\n    public int height() {\n        return (int) this.rootInstance.transform.height();\n    }\n\n    // ---\n\n    private Visibility visibility = Visibility.SHOW;\n\n    @Override\n    public void update(ToastManager manager, long time) {\n        if (this.timeout != null && time > this.timeout.toMillis()) {\n            this.visibility = Visibility.HIDE;\n        }\n\n        var tickCounter = Minecraft.getInstance().getDeltaTracker();\n        this.app.processEvents(\n            tickCounter.getGameTimeDeltaTicks()\n        );\n    }\n\n    @Override\n    public Visibility getWantedVisibility() {\n        return this.visibility;\n    }\n\n    @Override\n    public Object getToken() {\n        return this.token;\n    }\n}\n\nclass BraidToastProvider extends InheritedWidget {\n\n    public final BraidToast toast;\n\n    public BraidToastProvider(BraidToast toast, Widget child) {\n        super(child);\n        this.toast = toast;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/BraidTooltipComponent.java",
    "content": "package io.wispforest.owo.braid.util;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.EventBinding;\nimport io.wispforest.owo.braid.core.Surface;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Align;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport org.apache.commons.lang3.mutable.MutableObject;\n\nimport java.lang.ref.Cleaner;\n\npublic class BraidTooltipComponent implements ClientTooltipComponent {\n\n    private final AppState app;\n    private final EmbedderRoot.Instance instance;\n\n    public BraidTooltipComponent(Widget widget) {\n        var embedderInstance = new MutableObject<EmbedderRoot.Instance>();\n        this.app = new AppState(\n            Owo.LOGGER,\n            AppState.formatName(\"BraidTooltipComponent\", widget),\n            Minecraft.getInstance(),\n            new Surface.Default(),\n            new EventBinding.Headless(),\n            new Align(\n                Alignment.TOP_LEFT,\n                new EmbedderRoot(\n                    embedderInstance::setValue,\n                    widget\n                )\n            )\n        );\n\n        this.app.processEvents(0);\n        this.instance = embedderInstance.getValue();\n\n        APP_CLEANER.register(this, new CleanCallback(this.app));\n    }\n\n    @Override\n    public void renderImage(Font font, int x, int y, int width, int height, GuiGraphics context) {\n        context.push().translate(x, y);\n        this.app.draw(context);\n        context.pop();\n    }\n\n    @Override\n    public int getWidth(Font font) {\n        return (int) this.instance.transform.width();\n    }\n\n    @Override\n    public int getHeight(Font font) {\n        return (int) this.instance.transform.height();\n    }\n\n    // ---\n\n    private static final Cleaner APP_CLEANER = Cleaner.create();\n\n    private record CleanCallback(AppState app) implements Runnable {\n        @Override\n        public void run() {\n            Minecraft.getInstance().schedule(this.app::dispose);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/EmbedderRoot.java",
    "content": "package io.wispforest.owo.braid.util;\n\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.function.Consumer;\n\npublic class EmbedderRoot extends SingleChildInstanceWidget {\n\n    public final Consumer<Instance> instanceListener;\n\n    public EmbedderRoot(Consumer<Instance> instanceListener, Widget child) {\n        super(child);\n        this.instanceListener = instanceListener;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        var instance = new Instance(this);\n        this.instanceListener.accept(instance);\n\n        return instance;\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<EmbedderRoot> {\n        public Instance(EmbedderRoot widget) {\n            super(widget);\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/kdl/BraidKdlEndecs.java",
    "content": "package io.wispforest.owo.braid.util.kdl;\n\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationAttribute;\nimport io.wispforest.endec.StructEndec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.commands.arguments.blocks.BlockStateParser;\nimport net.minecraft.commands.arguments.item.ItemParser;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.world.item.ItemStack;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2f;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic final class BraidKdlEndecs {\n    private BraidKdlEndecs() {}\n\n    public static final SerializationAttribute.WithValue<Map<String, Consumer<@Nullable Object>>> HANDLERS = SerializationAttribute.withValue(\"braid_handlers\");\n\n    public static final Endec<Alignment> ALIGNMENT = Endec.STRING.xmap(\n        s -> switch (s) {\n            case \"top_left\" -> Alignment.TOP_LEFT;\n            case \"top\" -> Alignment.TOP;\n            case \"top_right\" -> Alignment.TOP_RIGHT;\n            case \"left\" -> Alignment.LEFT;\n            case \"center\" -> Alignment.CENTER;\n            case \"right\" -> Alignment.RIGHT;\n            case \"bottom_left\" -> Alignment.BOTTOM_LEFT;\n            case \"bottom\" -> Alignment.BOTTOM;\n            case \"bottom_right\" -> Alignment.BOTTOM_RIGHT;\n            default -> throw new IllegalStateException(\"invalid alignment type: \" + s);\n        },\n        alignment -> {\n            throw new UnsupportedOperationException(\"cannot serialize arbitrary alignment into a string\");\n        }\n    );\n\n    public static final Endec<Color> COLOR = Endec.INT.xmap(Color::new, Color::argb);\n\n    public static final Endec<LayoutAxis> LAYOUT_AXIS = Endec.STRING.xmap(\n        s -> switch (s) {\n            case \"column\" -> LayoutAxis.VERTICAL;\n            case \"row\" -> LayoutAxis.HORIZONTAL;\n            default -> throw new IllegalStateException(\"invalid layout axis: \" + s);\n        },\n        layoutAxis -> switch (layoutAxis) {\n            case VERTICAL -> \"column\";\n            case HORIZONTAL -> \"row\";\n        }\n    );\n    public static final Endec<CrossAxisAlignment> CROSS_AXIS_ALIGNMENT = Endec.forEnum(CrossAxisAlignment.class, false);\n    public static final Endec<MainAxisAlignment> MAIN_AXIS_ALIGNMENT = Endec.forEnum(MainAxisAlignment.class, false);\n\n    public static final Endec<Vector2f> VECTOR2F = Endec.FLOAT.listOf().validate(floats -> {\n        if (floats.size() != 2) {\n            throw new IllegalStateException(\"Vector2f array must have two elements\");\n        }\n    }).xmap(\n        components -> new Vector2f(components.get(0), components.get(1)),\n        vector -> List.of(vector.x, vector.y)\n    );\n\n    private sealed interface TransformStep {\n        record Translate(Vector2f translation) implements TransformStep {\n            public static final StructEndec<Translate> ENDEC = StructEndecBuilder.of(\n                VECTOR2F.fieldOf(\"@arguments\", Translate::translation),\n                Translate::new\n            );\n        }\n\n        record Scale(Vector2f scaling) implements TransformStep {\n            public static final StructEndec<Scale> ENDEC = StructEndecBuilder.of(\n                VECTOR2F.fieldOf(\"@arguments\", Scale::scaling),\n                Scale::new\n            );\n        }\n\n        record Rotate(float angle) implements TransformStep {\n            public static final StructEndec<Rotate> ENDEC = StructEndecBuilder.of(\n                Endec.FLOAT.fieldOf(\"@argument\", Rotate::angle),\n                Rotate::new\n            );\n        }\n\n        StructEndec<TransformStep> ENDEC = Endec.dispatchedStruct(\n            variant -> switch (variant) {\n                case \"translate\" -> Translate.ENDEC;\n                case \"scale\" -> Scale.ENDEC;\n                case \"rotate\" -> Rotate.ENDEC;\n                default -> throw new IllegalStateException(\"invalid transform step: \" + variant);\n            },\n            step -> switch (step) {\n                case Translate ignored -> \"translate\";\n                case Scale ignored -> \"scale\";\n                case Rotate ignored -> \"rotate\";\n            },\n            Endec.STRING,\n            \"@name\"\n        );\n    }\n\n    public static final StructEndec<Matrix3x2f> TRANSFORM_MATRIX_2D = StructEndecBuilder.of(\n        TransformStep.ENDEC.listOf().fieldOf(\"@children\", s -> {throw new UnsupportedOperationException(\"cannot serialize a matrix into transform steps\");}),\n        transformSteps -> {\n            var result = new Matrix3x2f();\n            for (var step : transformSteps) {\n                switch (step) {\n                    case TransformStep.Translate(var translation) -> result.translate(translation);\n                    case TransformStep.Scale(var scaling) -> result.scale(scaling);\n                    case TransformStep.Rotate(var angle) -> result.rotate((float) Math.toRadians(angle));\n                }\n            }\n            return result;\n        }\n    );\n\n    public static final Endec<ItemStack> ITEM_STACK_STRING = Endec.STRING.xmap(\n        s -> {\n            try {\n                var result = new ItemParser(Minecraft.getInstance().level.registryAccess()).parse(new StringReader(s));\n                var stack = result.item().value().getDefaultInstance();\n                stack.applyComponents(result.components());\n\n                return stack;\n            } catch (CommandSyntaxException e) {\n                throw new IllegalStateException(\"invalid item stack: \" + s, e);\n            }\n        },\n        stack -> { throw new UnsupportedOperationException(\"cannot serialize an item stack to a string\"); }\n    );\n\n    public static final Endec<BlockStateParser.BlockResult> BLOCK_STRING = Endec.STRING.xmap(\n        s -> {\n            try {\n                return BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, s, true);\n            } catch (CommandSyntaxException e) {\n                throw new IllegalStateException(\"invalid block state: \" + s, e);\n            }\n        },\n        blockState -> { throw new UnsupportedOperationException(\"cannot serialize a block state to a string\"); }\n    );\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/kdl/KdlDeserializer.java",
    "content": "package io.wispforest.owo.braid.util.kdl;\n\nimport dev.kdl.KdlNode;\nimport dev.kdl.KdlValue;\nimport io.wispforest.endec.*;\nimport io.wispforest.endec.util.RecursiveDeserializer;\nimport org.jspecify.annotations.Nullable;\n\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\npublic class KdlDeserializer extends RecursiveDeserializer<KdlElement> implements SelfDescribedDeserializer<KdlElement> {\n\n    public final List<KdlMapper> mappers;\n\n    public KdlDeserializer(KdlNode rootNode, List<KdlMapper> mappers) {\n        super(new KdlElement.KdlNodeElement(rootNode));\n        this.mappers = mappers;\n    }\n\n    @Override\n    public <S> void readAny(SerializationContext ctx, Serializer<S> visitor) {\n        this.decodeElement(ctx, visitor, this.getValue());\n    }\n\n    private final Endec<KdlElement> elementEndec = Endec.of(\n        this::decodeElement,\n        (ctx, deserializer) -> { throw new AssertionError(\"unreachable\"); }\n    );\n\n    private void decodeElement(SerializationContext ctx, Serializer<?> visitor, KdlElement element) {\n        switch (element) {\n            case KdlElement.KdlValueElement(var value) -> {\n                if (value.isBoolean()) {\n                    visitor.writeBoolean(ctx, (Boolean) value.value());\n                } else if (value.isNumber()) {\n                    visitor.writeLong(ctx, ((Number) value.value()).longValue());\n                } else if (value.isString()) {\n                    visitor.writeString(ctx, (String) value.value());\n                } else if (value.isNull()) {\n                    visitor.writeOptional(ctx, this.elementEndec, Optional.empty());\n                } else {\n                    throw new UnsupportedOperationException(\"unknown KDL value type\");\n                }\n            }\n            case KdlElement.KdlNodeElement(var node) -> {\n                try (var state = visitor.struct()) {\n                    node.properties().forEach(entry -> {\n                        state.field(entry.getKey(), ctx, this.elementEndec, new KdlElement.KdlValueElement(entry.getValue().getFirst()));\n                    });\n                    for (var mapper : this.mappers) {\n                        if (!mapper.export().apply(node)) {\n                            continue;\n                        }\n\n                        state.field(mapper.key(), ctx, this.elementEndec, mapper.get().apply(node));\n                    }\n                }\n            }\n            case KdlElement.KdlElementList(var elements) -> {\n                try (var state = visitor.sequence(ctx, this.elementEndec, elements.size())) {\n                    elements.forEach(state::element);\n                }\n            }\n        }\n    }\n\n    @Override\n    public byte readByte(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).byteValue();\n    }\n\n    @Override\n    public short readShort(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).shortValue();\n    }\n\n    @Override\n    public int readInt(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).intValue();\n    }\n\n    @Override\n    public long readLong(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).longValue();\n    }\n\n    @Override\n    public float readFloat(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).floatValue();\n    }\n\n    @Override\n    public double readDouble(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).doubleValue();\n    }\n\n    @Override\n    public int readVarInt(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).intValue();\n    }\n\n    @Override\n    public long readVarLong(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Number.class).longValue();\n    }\n\n    @Override\n    public boolean readBoolean(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, Boolean.class);\n    }\n\n    @Override\n    public String readString(SerializationContext ctx) {\n        return this.expectPrimitive(ctx, String.class);\n    }\n\n    @Override\n    public byte[] readBytes(SerializationContext ctx) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public <V> Optional<V> readOptional(SerializationContext ctx, Endec<V> endec) {\n        var value = this.getValue();\n        return !(value instanceof KdlElement.KdlValueElement(var kdlValue) && kdlValue.isNull())\n            ? Optional.of(endec.decode(ctx, this))\n            : Optional.empty();\n    }\n\n    private <K extends KdlElement> K expectElement(SerializationContext ctx, Class<K> clazz) {\n        var value = this.getValue();\n        if (!(clazz.isAssignableFrom(value.getClass()))) {\n            ctx.throwMalformedInput(\"Expected a \" + KdlElement.KdlValueElement.class.getSimpleName() + \", found a \" + value.getClass().getSimpleName());\n        }\n        return (K) value;\n    }\n\n    private <V> V expectPrimitive(SerializationContext ctx, Class<V> clazz) {\n        var kdlValue = expectElement(ctx, KdlElement.KdlValueElement.class).value();\n        if (!clazz.isAssignableFrom(kdlValue.value().getClass())) {\n            ctx.throwMalformedInput(\"Expected a \" + clazz.getSimpleName() + \", found a \" + kdlValue.value().getClass().getSimpleName());\n        }\n        //noinspection unchecked\n        return (V) kdlValue.value();\n    }\n\n    @Override\n    public <E> Deserializer.Sequence<E> sequence(SerializationContext ctx, Endec<E> elementEndec) {\n        return new Sequence<>(ctx, elementEndec, expectElement(ctx, KdlElement.KdlElementList.class).elements());\n    }\n\n    @Override\n    public <V> Map<V> map(SerializationContext ctx, Endec<V> valueEndec) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Deserializer.Struct struct(SerializationContext ctx) {\n        return new Struct(expectElement(ctx, KdlElement.KdlNodeElement.class).node());\n    }\n\n    private class Struct implements Deserializer.Struct {\n\n        public final KdlNode node;\n\n        private Struct(KdlNode node) {\n            this.node = node;\n        }\n\n        @Override\n        public @Nullable <F> F field(String name, SerializationContext ctx, Endec<F> endec, @org.jetbrains.annotations.Nullable Supplier<F> defaultValueFactory) {\n            var element = this.tryMap(name);\n            if (element == null && this.node.properties().hasProperty(name)) {\n                element = new KdlElement.KdlValueElement(node.properties().getValue(name).get());\n            }\n\n            if (element == null) {\n                if (defaultValueFactory != null) {\n                    return defaultValueFactory.get();\n                }\n\n                throw new IllegalStateException(\"Required property \" + name + \" is missing from serialized data\");\n            }\n\n            var javaMoment = element;\n            return KdlDeserializer.this.frame(\n                () -> javaMoment,\n                () -> endec.decode(ctx, KdlDeserializer.this)\n            );\n        }\n\n        private @Nullable KdlElement tryMap(String key) {\n            if (key.startsWith(\".\")) {\n                var maybeChild = this.node.children().stream().filter(node -> node.name().equals(key)).findFirst();\n\n                return maybeChild.map(KdlElement.KdlNodeElement::new).orElse(null);\n            }\n\n            var mapper = KdlDeserializer.this.mappers.stream().filter(element -> Objects.equals(element.key(), key)).findFirst().orElse(null);\n            if (mapper == null) {\n                return null;\n            }\n\n            return mapper.get().apply(this.node);\n        }\n    }\n\n    private class Sequence<V> implements Deserializer.Sequence<V> {\n\n        public final SerializationContext ctx;\n        public final Endec<V> elementEndec;\n        public final Iterator<KdlElement> iterator;\n        public final int size;\n\n        private Sequence(SerializationContext ctx, Endec<V> elementEndec, List<KdlElement> elements) {\n            this.ctx = ctx;\n            this.elementEndec = elementEndec;\n            this.iterator = elements.iterator();\n            this.size = elements.size();\n        }\n\n        @Override\n        public int estimatedSize() {\n            return this.size;\n        }\n\n        @Override\n        public boolean hasNext() {\n            return this.iterator.hasNext();\n        }\n\n        @Override\n        public V next() {\n            var value = this.iterator.next();\n\n            return KdlDeserializer.this.frame(\n                () -> value,\n                () -> this.elementEndec.decode(this.ctx, KdlDeserializer.this)\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/kdl/KdlElement.java",
    "content": "package io.wispforest.owo.braid.util.kdl;\n\nimport dev.kdl.KdlNode;\nimport dev.kdl.KdlValue;\n\nimport java.util.List;\n\npublic sealed interface KdlElement {\n    record KdlElementList(List<KdlElement> elements) implements KdlElement {}\n    record KdlNodeElement(KdlNode node) implements KdlElement {}\n    record KdlValueElement(KdlValue<?> value) implements KdlElement {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/kdl/KdlEntityWidget.java",
    "content": "package io.wispforest.owo.braid.util.kdl;\n\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.object.EntityWidget;\nimport net.minecraft.IdentifierException;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.TagParser;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.EntitySpawnReason;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.level.storage.TagValueInput;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.NoSuchElementException;\n\npublic class KdlEntityWidget extends StatefulWidget {\n\n    public final double scale;\n    public final EntitySpec spec;\n\n    public final EntityWidget.DisplayMode mode;\n    public final boolean scaleToFit;\n    public final boolean showNametag;\n\n    public KdlEntityWidget(double scale, EntitySpec spec, EntityWidget.DisplayMode mode, boolean scaleToFit, boolean showNametag) {\n        this.scale = scale;\n        this.spec = spec;\n        this.mode = mode;\n        this.scaleToFit = scaleToFit;\n        this.showNametag = showNametag;\n    }\n\n    @Override\n    public WidgetState<KdlEntityWidget> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<KdlEntityWidget> {\n\n        private Entity entity;\n\n        @Override\n        public void init() {\n            this.recreateEntity();\n        }\n\n        @Override\n        public void didUpdateWidget(KdlEntityWidget oldWidget) {\n            if (!this.widget().spec.equals(oldWidget.spec)) {\n                this.recreateEntity();\n            }\n        }\n\n        private void recreateEntity() {\n            var level = AppState.of(this.context()).client().level;\n\n            var entity = this.widget().spec.type.create(level, EntitySpawnReason.LOAD);\n            if (this.widget().spec.nbt != null) {\n                entity.load(TagValueInput.create(new ProblemReporter.ScopedCollector(Owo.LOGGER), level.registryAccess(), this.widget().spec.nbt));\n            }\n\n            this.setState(() -> {\n                this.entity = entity;\n            });\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new EntityWidget(\n                this.widget().scale,\n                this.entity,\n                widget -> widget\n                    .displayMode(this.widget().mode)\n                    .scaleToFit(this.widget().scaleToFit)\n                    .showNametag(this.widget().showNametag)\n            );\n        }\n    }\n\n    public record EntitySpec(EntityType<?> type, @Nullable CompoundTag nbt) {\n        public static final Endec<EntitySpec> STRING_ENDEC = Endec.STRING.xmap(\n            s -> {\n                try {\n                    CompoundTag nbt = null;\n\n                    int nbtIndex = s.indexOf('{');\n                    if (nbtIndex != -1) {\n\n                        nbt = TagParser.parseCompoundAsArgument(new StringReader(s.substring(nbtIndex)));\n                        s = s.substring(0, nbtIndex);\n                    }\n\n                    var entityType = BuiltInRegistries.ENTITY_TYPE.getOptional(Identifier.parse(s)).orElseThrow();\n                    return new EntitySpec(entityType, nbt);\n                } catch (CommandSyntaxException | NoSuchElementException | IdentifierException e) {\n                    throw new IllegalStateException(\"invalid entity: \" + s, e);\n                }\n            },\n            spec -> { throw new UnsupportedOperationException(\"cannot serialize an entity spec to a string\"); }\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/kdl/KdlMapper.java",
    "content": "package io.wispforest.owo.braid.util.kdl;\n\nimport dev.kdl.KdlNode;\nimport dev.kdl.KdlString;\n\nimport java.util.List;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic record KdlMapper(Function<KdlNode, Boolean> export, String key, Function<KdlNode, KdlElement> get, BiFunction<KdlNode, KdlElement, KdlNode> set) {\n    public static final List<KdlMapper> DEFAULT_MAPPERS = List.of(\n        new KdlMapper(\n            kdlNode -> true,\n            \"@name\",\n            node -> new KdlElement.KdlValueElement(new KdlString(node.name())),\n            (node, element) -> node.mutate().name((String) ((KdlElement.KdlValueElement) element).value().value()).build()\n        ),\n        new KdlMapper(\n            kdlNode -> kdlNode.arguments().size() == 1,\n            \"@argument\",\n            node -> !node.arguments().isEmpty() ? new KdlElement.KdlValueElement(node.arguments().getFirst()) : null,\n            (node, element) -> node.mutate().argument(((KdlElement.KdlValueElement) element).value()).build()\n        ),\n        new KdlMapper(\n            kdlNode -> kdlNode.arguments().size() > 1,\n            \"@arguments\",\n            node -> new KdlElement.KdlElementList(node.arguments().stream().<KdlElement>map(KdlElement.KdlValueElement::new).toList()),\n            (node, element) -> {\n                var builder = node.mutate();\n                ((KdlElement.KdlElementList) element).elements()\n                    .stream()\n                    .map(kdlElement -> ((KdlElement.KdlValueElement) kdlElement).value())\n                    .forEach(builder::argument);\n                return builder.build();\n            }\n        ),\n        new KdlMapper(\n            kdlNode -> kdlNode.children().size() == 1,\n            \"@child\",\n            node -> {\n                var candidates = node.children().stream().filter(kdlNode -> !kdlNode.name().startsWith(\".\")).toList();\n                return !candidates.isEmpty() ? new KdlElement.KdlNodeElement(candidates.getFirst()) : null;\n            },\n            (node, element) -> node.mutate().child(((KdlElement.KdlNodeElement) element).node()).build()\n        ),\n        new KdlMapper(\n            kdlNode -> kdlNode.children().size() > 1,\n            \"@children\",\n            node -> new KdlElement.KdlElementList(\n                node.children().stream()\n                    .filter(kdlNode -> !kdlNode.name().startsWith(\".\"))\n                    .<KdlElement>map(KdlElement.KdlNodeElement::new)\n                    .toList()\n            ),\n            (node, element) -> {\n                var builder = node.mutate();\n                ((KdlElement.KdlElementList) element).elements().stream()\n                    .map(kdlElement -> ((KdlElement.KdlNodeElement) kdlElement).node())\n                    .forEach(builder::child);\n                return builder.build();\n            }\n        )\n    );\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/kdl/WidgetEndec.java",
    "content": "package io.wispforest.owo.braid.util.kdl;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SelfDescribedDeserializer;\nimport io.wispforest.endec.StructEndec;\nimport io.wispforest.endec.format.java.JavaSerializer;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.Button;\nimport io.wispforest.owo.braid.widgets.button.MessageButton;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Flex;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.grid.Grid;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.object.BlockWidget;\nimport io.wispforest.owo.braid.widgets.object.EntityWidget;\nimport io.wispforest.owo.braid.widgets.object.ItemStackWidget;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport net.minecraft.client.resources.model.Material;\nimport net.minecraft.commands.arguments.blocks.BlockStateParser;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.contents.TranslatableContents;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.item.ItemDisplayContext;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class WidgetEndec {\n\n    private static final Map<String, StructEndec<? extends Widget>> REGISTRY = new HashMap<>();\n    private static final Map<Class<? extends Widget>, String> WIDGET_TYPE_NAMES = new HashMap<>();\n\n    public static final Endec<Widget> ROOT = Endec.dispatchedStruct(\n        variant -> {\n            var endec = REGISTRY.get(variant);\n            if (endec == null) {\n                throw new IllegalStateException(\"Unknown widget type: \" + variant);\n            }\n\n            return endec;\n        },\n        widget -> WIDGET_TYPE_NAMES.get(widget.getClass()),\n        Endec.STRING,\n        \"@name\"\n    );\n\n\n    public static <W extends Widget> void register(Identifier key, Class<W> widgetClass, StructEndec<W> endec) {\n        register(key.toLanguageKey(), widgetClass, endec);\n    }\n\n    @ApiStatus.Internal\n    public static <W extends Widget> void register(String key, Class<W> widgetClass, StructEndec<W> endec) {\n        if (REGISTRY.containsKey(key)) {\n            throw new IllegalArgumentException(\"Duplicate widget endec key: \" + key);\n        }\n\n        REGISTRY.put(key, endec);\n        WIDGET_TYPE_NAMES.put(widgetClass, key);\n    }\n\n    // ---\n\n    static {\n        register(\n            \"align\",\n            Align.class,\n            StructEndecBuilder.of(\n                BraidKdlEndecs.ALIGNMENT.fieldOf(\"@argument\", s -> s.alignment),\n                Endec.DOUBLE.nullableOf().optionalFieldOf(\"width_factor\", s -> s.widthFactor.isPresent() ? s.widthFactor.getAsDouble() : null, (Double) null),\n                Endec.DOUBLE.nullableOf().optionalFieldOf(\"height_factor\", s -> s.heightFactor.isPresent() ? s.heightFactor.getAsDouble() : null, (Double) null),\n                ROOT.fieldOf(\"@child\", s -> s.child),\n                Align::new\n            )\n        );\n\n        register(\n            \"center\",\n            Center.class,\n            StructEndecBuilder.of(\n                Endec.DOUBLE.nullableOf().optionalFieldOf(\"width_factor\", s -> s.widthFactor.isPresent() ? s.widthFactor.getAsDouble() : null, (Double) null),\n                Endec.DOUBLE.nullableOf().optionalFieldOf(\"height_factor\", s -> s.heightFactor.isPresent() ? s.heightFactor.getAsDouble() : null, (Double) null),\n                ROOT.fieldOf(\"@child\", s -> s.child),\n                Center::new\n            )\n        );\n\n        register(\n            \"padding\",\n            Padding.class,\n            StructEndecBuilder.of(\n                Endec.DOUBLE.optionalOf().optionalFieldOf(\"@argument\", s -> Optional.<Double>empty(), Optional::empty),\n                Endec.DOUBLE.optionalOf().optionalFieldOf(\"horizontal\", s -> Optional.<Double>empty(), Optional::empty),\n                Endec.DOUBLE.optionalOf().optionalFieldOf(\"vertical\", s -> Optional.<Double>empty(), Optional::empty),\n                Endec.DOUBLE.optionalOf().optionalFieldOf(\"top\", s -> Optional.of(s.insets.top()), Optional::empty),\n                Endec.DOUBLE.optionalOf().optionalFieldOf(\"bottom\", s -> Optional.of(s.insets.bottom()), Optional::empty),\n                Endec.DOUBLE.optionalOf().optionalFieldOf(\"left\", s -> Optional.of(s.insets.left()), Optional::empty),\n                Endec.DOUBLE.optionalOf().optionalFieldOf(\"right\", s -> Optional.of(s.insets.right()), Optional::empty),\n                ROOT.optionalOf().optionalFieldOf(\"@child\", s -> Optional.ofNullable(s.child), Optional.empty()),\n                (all, horizontal, vertical, top, bottom, left, right, child) -> {\n                    var dTop = top.orElse(vertical.orElse(all.orElse(0.0)));\n                    var dBottom = bottom.orElse(vertical.orElse(all.orElse(0.0)));\n                    var dLeft = left.orElse(horizontal.orElse(all.orElse(0.0)));\n                    var dRight = right.orElse(horizontal.orElse(all.orElse(0.0)));\n\n                    return new Padding(\n                        Insets.of(dTop, dBottom, dLeft, dRight),\n                        child.orElse(null)\n                    );\n                }\n            )\n        );\n\n        register(\n            \"sized\",\n            Sized.class,\n            StructEndecBuilder.of(\n                Endec.DOUBLE.nullableOf().optionalFieldOf(\"width\", s -> s.width, (Double) null),\n                Endec.DOUBLE.nullableOf().optionalFieldOf(\"height\", s -> s.height, (Double) null),\n                ROOT.fieldOf(\"@child\", s -> s.child),\n                Sized::new\n            )\n        );\n\n        //noinspection unchecked\n        register(\n            \"flex\",\n            Flex.class,\n            StructEndecBuilder.of(\n                BraidKdlEndecs.LAYOUT_AXIS.fieldOf(\"@argument\", s -> s.mainAxis),\n                BraidKdlEndecs.MAIN_AXIS_ALIGNMENT.optionalFieldOf(\"main_axis_alignment\", s -> s.mainAxisAlignment, MainAxisAlignment.START),\n                BraidKdlEndecs.CROSS_AXIS_ALIGNMENT.optionalFieldOf(\"cross_axis_alignment\", s -> s.crossAxisAlignment, CrossAxisAlignment.START),\n                ROOT.listOf().fieldOf(\"@children\", s -> (java.util.List<Widget>) s.children),\n                (mainAxis, mainAxisAlignment, crossAxisAlignment, children) ->\n                    new Flex(mainAxis, mainAxisAlignment, crossAxisAlignment, null, children)\n            )\n        );\n\n        register(\n            \"flexible\",\n            Flexible.class,\n            StructEndecBuilder.of(\n                Endec.DOUBLE.optionalFieldOf(\"flex_factor\", s -> s.flexFactor, 1.0),\n                ROOT.fieldOf(\"@child\", s -> s.child),\n                Flexible::new\n            )\n        );\n\n        //noinspection unchecked\n        register(\n            \"stack\",\n            Stack.class,\n            StructEndecBuilder.of(\n                BraidKdlEndecs.ALIGNMENT.optionalFieldOf(\"alignment\", s -> s.alignment, Alignment.TOP_LEFT),\n                ROOT.listOf().fieldOf(\"@children\", s -> (java.util.List<Widget>) s.children),\n                Stack::new\n            )\n        );\n\n        register(\n            \"stack_base\",\n            StackBase.class,\n            StructEndecBuilder.of(\n                ROOT.fieldOf(\"@child\", s -> s.child),\n                StackBase::new\n            )\n        );\n\n        var tightCellFitEndec = Endec.unit(Grid.CellFit.tight());\n        var looseCellFitEndec = StructEndecBuilder.of(\n            BraidKdlEndecs.ALIGNMENT.fieldOf(\"@argument\", s -> ((Grid.CellFit.Loose)s).alignment),\n            Grid.CellFit::loose\n        );\n\n        var cellFitEndec = Endec.dispatchedStruct(\n            s -> switch (s) {\n                case \"tight\" -> tightCellFitEndec;\n                case \"loose\" -> looseCellFitEndec;\n                default -> throw new IllegalStateException(\"invalid cell fit: \" + s);\n            },\n            cellFit -> switch (cellFit) {\n                case Grid.CellFit.Tight ignored -> \"tight\";\n                case Grid.CellFit.Loose ignored -> \"loose\";\n            },\n            Endec.STRING,\n            \"@name\"\n        );\n\n        //noinspection unchecked\n        register(\n            \"grid\",\n            Grid.class,\n            StructEndecBuilder.of(\n                BraidKdlEndecs.LAYOUT_AXIS.fieldOf(\"@argument\", s -> s.mainAxis),\n                Endec.INT.fieldOf(\"cross_axis_cells\", s -> s.crossAxisCells),\n                StructEndecBuilder.of(cellFitEndec.fieldOf(\"@child\", s -> s), cellFit -> cellFit).fieldOf(\".fit\", s -> s.cellFit),\n                ROOT.listOf().fieldOf(\"@children\", s ->  (java.util.List<Widget>) s.children),\n                Grid::new\n            )\n        );\n\n        var labelStyleEndec = StructEndecBuilder.of(\n            BraidKdlEndecs.ALIGNMENT.nullableOf().optionalFieldOf(\"text_alignment\", LabelStyle::textAlignment, (Alignment) null),\n            BraidKdlEndecs.COLOR.nullableOf().optionalFieldOf(\"base_color\", LabelStyle::baseColor, (Color) null),\n            Endec.BOOLEAN.nullableOf().optionalFieldOf(\"shadow\", LabelStyle::shadow, (Boolean) null),\n            (alignment, color, shadow) ->\n                new LabelStyle(alignment, color, null, shadow)\n        );\n\n        register(\n            \"label\",\n            Label.class,\n            StructEndecBuilder.of(\n                labelStyleEndec.nullableOf().optionalFieldOf(\".style\", s -> s.style, (LabelStyle) null),\n                Endec.BOOLEAN.optionalFieldOf(\"soft_wrap\", s -> s.softWrap, true),\n                Endec.forEnum(Label.Overflow.class, false).optionalFieldOf(\"overflow\", s -> s.overflow, Label.Overflow.CLIP),\n                Endec.BOOLEAN.optionalFieldOf(\"translate\", s -> s.text.getContents() instanceof TranslatableContents, false),\n                Endec.STRING.fieldOf(\"@argument\", s -> s.text.getString()),\n                (style, softWrap, overflow, translate, text) -> new Label(\n                    style,\n                    softWrap,\n                    overflow,\n                    translate ? Component.translatable(text) : Component.literal(text)\n                )\n            )\n        );\n\n        register(\n            \"texture\",\n            TextureWidget.class,\n            StructEndecBuilder.of(\n                MinecraftEndecs.IDENTIFIER.fieldOf(\"@argument\", s -> s.texture),\n                Endec.forEnum(TextureWidget.Wrap.class, false).optionalFieldOf(\"wrap\", s -> s.wrap, TextureWidget.Wrap.STRETCH),\n                Endec.forEnum(TextureWidget.Filter.class, false).optionalFieldOf(\"filter\", s -> s.filter, TextureWidget.Filter.TEXTURE_DEFAULT),\n                BraidKdlEndecs.COLOR.optionalFieldOf(\"color\", s -> s.color, Color.WHITE),\n                TextureWidget::new\n            )\n        );\n\n        register(\n            \"sprite\",\n            SpriteWidget.class,\n            StructEndecBuilder.of(\n                MinecraftEndecs.IDENTIFIER.fieldOf(\"@argument\", s -> s.spriteIdentifier.texture()),\n                MinecraftEndecs.IDENTIFIER.optionalFieldOf(\"atlas\", s -> s.spriteIdentifier.atlasLocation(), SpriteWidget.GUI_ATLAS_ID),\n                (id, atlas) -> new SpriteWidget(new Material(atlas, id))\n            )\n        );\n\n        register(\n            \"box\",\n            Box.class,\n            StructEndecBuilder.of(\n                BraidKdlEndecs.COLOR.fieldOf(\"@argument\", s -> s.color),\n                Endec.BOOLEAN.optionalFieldOf(\"outline\", s -> s.outline, false),\n                ROOT.nullableOf().optionalFieldOf(\"@child\", s -> s.child, (Widget) null),\n                Box::new\n            )\n        );\n\n        register(\n            \"transform\",\n            Transform.class,\n            StructEndecBuilder.of(\n                BraidKdlEndecs.TRANSFORM_MATRIX_2D.fieldOf(\".steps\", s -> s.matrix),\n                ROOT.fieldOf(\"@child\", s -> s.child),\n                Transform::new\n            )\n        );\n\n        register(\n            \"item\",\n            ItemStackWidget.class,\n            StructEndecBuilder.of(\n                Endec.forEnum(ItemDisplayContext.class, false).optionalFieldOf(\"display_context\", ItemStackWidget::displayContext, ItemDisplayContext.GUI),\n                Endec.BOOLEAN.optionalFieldOf(\"show_overlay\", ItemStackWidget::showOverlay, true),\n                Endec.forEnum(ItemStackWidget.LightOverride.class, false).nullableOf().optionalFieldOf(\"light_override\", ItemStackWidget::lightOverride, (ItemStackWidget.LightOverride) null),\n                BraidKdlEndecs.ITEM_STACK_STRING.fieldOf(\"@argument\", s -> s.stack),\n                (displayContext, showOverlay, lightOverride, stack) -> new ItemStackWidget(\n                    stack,\n                    widget -> widget\n                        .displayContext(displayContext)\n                        .showOverlay(showOverlay)\n                        .lightOverride(lightOverride)\n                )\n            )\n        );\n\n        register(\n            \"block\",\n            BlockWidget.class,\n            StructEndecBuilder.of(\n                BraidKdlEndecs.BLOCK_STRING.fieldOf(\"@argument\", s -> new BlockStateParser.BlockResult(s.blockState, s.blockState.getValues(), s.blockEntityNbt)),\n                blockResult -> new BlockWidget(blockResult.blockState(), blockResult.nbt())\n            )\n        );\n\n        register(\n            \"entity\",\n            KdlEntityWidget.class,\n            StructEndecBuilder.of(\n                Endec.DOUBLE.optionalFieldOf(\"scale\", s -> s.scale, 1.0),\n                KdlEntityWidget.EntitySpec.STRING_ENDEC.fieldOf(\"@argument\", s -> s.spec),\n                Endec.forEnum(EntityWidget.DisplayMode.class, false).optionalFieldOf(\"mode\", s -> s.mode, EntityWidget.DisplayMode.FIXED),\n                Endec.BOOLEAN.optionalFieldOf(\"scale_to_fit\", s -> s.scaleToFit, true),\n                Endec.BOOLEAN.optionalFieldOf(\"show_nametag\", s -> s.showNametag, false),\n                KdlEntityWidget::new\n            )\n        );\n\n        var handlerEndec = Endec.STRING\n            .optionalOf()\n            .xmapWithContext(\n                (ctx, maybeHandlerId) -> maybeHandlerId.flatMap(handlerId -> {\n                    var handler = ctx.getAttributeValue(BraidKdlEndecs.HANDLERS).get(handlerId);\n                    if (handler == null) {\n                        throw new UnsupportedOperationException(\"missing handler with id: \" + handlerId);\n                    }\n\n                    return Optional.of(handler);\n                }),\n                (context, o) -> { throw new UnsupportedOperationException(\"cannot serialize a braid kdl handler\"); }\n            );\n\n        var handlerArgEndec = Endec.of(\n            (ctx, serializer, o) -> { throw new UnsupportedOperationException(\"cannot serialize a braid kdl handler argument\"); },\n            (ctx, deserializer) -> {\n                if (!(deserializer instanceof SelfDescribedDeserializer<?> selfDescribedDeserializer)) {\n                    throw new UnsupportedOperationException(\"can only deserialize braid kdl handler arguments from self-described input\");\n                }\n\n                var visitor = JavaSerializer.of();\n                selfDescribedDeserializer.readAny(ctx, visitor);\n\n                return visitor.result();\n            }\n        );\n\n        register(\n            \"message_button\",\n            MessageButton.class,\n            StructEndecBuilder.of(\n                Endec.STRING.fieldOf(\"message\", s -> s.text.getString()),\n                Endec.BOOLEAN.optionalFieldOf(\"translate_message\", s -> s.text.getContents() instanceof TranslatableContents, () -> false),\n                handlerEndec.fieldOf(\"handler\", s -> { throw new UnsupportedOperationException(\"cannot serialize a button callback\"); }),\n                handlerArgEndec.nullableOf().optionalFieldOf(\"handler_arg\", s -> { throw new UnsupportedOperationException(\"cannot serialize a button's callback argument\"); }, (Object) null),\n                (message, translateMessage, handler, handlerArg) -> new MessageButton(\n                    translateMessage\n                        ? Component.translatable(message)\n                        : Component.literal(message),\n                    handler.<Runnable>flatMap(o -> {\n                        return Optional.of(() -> o.accept(handlerArg));\n                    }).orElse(null)\n                )\n            )\n        );\n\n        register(\n            \"button\",\n            Button.class,\n            StructEndecBuilder.of(\n                handlerEndec.fieldOf(\"handler\", s -> { throw new UnsupportedOperationException(\"cannot serialize a button callback\"); }),\n                handlerArgEndec.nullableOf().optionalFieldOf(\"handler_arg\", s -> { throw new UnsupportedOperationException(\"cannot serialize a button's callback argument\"); }, (Object) null),\n                ROOT.fieldOf(\"@child\", s -> s.child),\n                (handler, handlerArg, child) -> {\n                    return new Button(\n                        handler.<Runnable>flatMap(o -> {\n                            return Optional.of(() -> o.accept(handlerArg));\n                        }).orElse(null),\n                        child\n                    );\n                }\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/layers/AnchorJustification.java",
    "content": "package io.wispforest.owo.braid.util.layers;\n\npublic record AnchorJustification(double anchorX, double anchorY, double widgetX, double widgetY) {\n    public static final AnchorJustification TOP_LEFT_TO_TOP_LEFT = new AnchorJustification(0, 0, 0, 0);\n    public static final AnchorJustification TOP_LEFT_TO_BOTTOM_LEFT = new AnchorJustification(0, 1, 0, 0);\n    public static final AnchorJustification TOP_LEFT_TO_TOP_RIGHT = new AnchorJustification(1, 0, 0, 0);\n    public static final AnchorJustification TOP_LEFT_TO_BOTTOM_RIGHT = new AnchorJustification(1, 1, 0, 0);\n\n    public static final AnchorJustification BOTTOM_LEFT_TOP_LEFT = new AnchorJustification(0, 0, 0, 1);\n    public static final AnchorJustification BOTTOM_LEFT_BOTTOM_LEFT = new AnchorJustification(0, 1, 0, 1);\n    public static final AnchorJustification BOTTOM_LEFT_TOP_RIGHT = new AnchorJustification(1, 0, 0, 1);\n    public static final AnchorJustification BOTTOM_LEFT_BOTTOM_RIGHT = new AnchorJustification(1, 1, 0, 1);\n\n    public static final AnchorJustification TOP_RIGHT_TO_TOP_LEFT = new AnchorJustification(0, 0, 1, 0);\n    public static final AnchorJustification TOP_RIGHT_TO_BOTTOM_LEFT = new AnchorJustification(0, 1, 1, 0);\n    public static final AnchorJustification TOP_RIGHT_TO_TOP_RIGHT = new AnchorJustification(1, 0, 1, 0);\n    public static final AnchorJustification TOP_RIGHT_TO_BOTTOM_RIGHT = new AnchorJustification(1, 1, 1, 0);\n\n    public static final AnchorJustification BOTTOM_RIGHT_TO_TOP_LEFT = new AnchorJustification(0, 0, 1, 1);\n    public static final AnchorJustification BOTTOM_RIGHT_TO_BOTTOM_LEFT = new AnchorJustification(0, 1, 1, 1);\n    public static final AnchorJustification BOTTOM_RIGHT_TO_TOP_RIGHT = new AnchorJustification(1, 0, 1, 1);\n    public static final AnchorJustification BOTTOM_RIGHT_TO_BOTTOM_RIGHT = new AnchorJustification(1, 1, 1, 1);\n\n    public static final AnchorJustification CENTER_TO_TOP_LEFT = new AnchorJustification(0, 0, .5, .5);\n    public static final AnchorJustification CENTER_TO_BOTTOM_LEFT = new AnchorJustification(0, 1, .5, .5);\n    public static final AnchorJustification CENTER_TO_TOP_RIGHT = new AnchorJustification(1, 0, .5, .5);\n    public static final AnchorJustification CENTER_TO_BOTTOM_RIGHT = new AnchorJustification(1, 1, .5, .5);\n    public static final AnchorJustification TOP_LEFT_TO_CENTER = new AnchorJustification(.5, .5, 0, 0);\n    public static final AnchorJustification BOTTOM_LEFT_TO_CENTER = new AnchorJustification(.5, .5, 0, 1);\n    public static final AnchorJustification TOP_RIGHT_TO_CENTER = new AnchorJustification(.5, .5, 1, 0);\n    public static final AnchorJustification BOTTOM_RIGHT_TO_CENTER = new AnchorJustification(.5, .5, 1, 1);\n    public static final AnchorJustification CENTER_TO_CENTER = new AnchorJustification(.5, .5, .5, .5);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/layers/BraidLayersBinding.java",
    "content": "package io.wispforest.owo.braid.util.layers;\n\nimport com.google.common.base.Suppliers;\nimport com.mojang.blaze3d.platform.cursor.CursorType;\nimport com.mojang.blaze3d.platform.cursor.CursorTypes;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.EventBinding;\nimport io.wispforest.owo.braid.core.Surface;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.core.events.*;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventStream;\nimport io.wispforest.owo.braid.widgets.overlay.Overlay;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.util.pond.OwoScreenExtension;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents;\nimport net.fabricmc.fabric.api.event.Event;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.Unit;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\npublic class BraidLayersBinding {\n\n    public static void add(Predicate<Screen> screenPredicate, Widget widget) {\n        LAYERS.add(new Layer(screenPredicate, widget));\n    }\n\n    // ---\n\n    @ApiStatus.Internal\n    public static boolean tryHandleEvent(Screen screen, UserEvent event) {\n        var app = ((OwoScreenExtension) screen).owo$getBraidLayersApp();\n        if (app == null) {\n            return false;\n        }\n\n        var slot = app.eventBinding.add(event);\n        app.processEvents(0);\n\n        return slot.handled();\n    }\n\n    @ApiStatus.Internal\n    public static void renderLayers(Screen screen, GuiGraphics graphics, double mouseX, double mouseY) {\n        var state = ((OwoScreenExtension) screen).owo$getBraidLayersState();\n        if (state == null) {\n            return;\n        }\n\n        state.refreshEvents.sink().onEvent(Unit.INSTANCE);\n        state.app.eventBinding.add(new MouseMoveEvent(mouseX, mouseY));\n\n        state.app.processEvents(Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaTicks());\n        state.app.draw(graphics);\n\n        var cursorStyle = ((LayerSurface) state.app.surface).currentCursorStyle;\n        if (cursorStyle != CursorStyle.NONE && CURSOR_MAPPINGS.get().containsKey(cursorStyle)) {\n            graphics.requestCursor(CURSOR_MAPPINGS.get().get(cursorStyle));\n        }\n    }\n\n    private static void setupLayers(Screen screen) {\n        var widgets = LAYERS.stream().filter(layer -> layer.screenPredicate.test(screen)).map(Layer::widget).toList();\n        if (widgets.isEmpty()) {\n            return;\n        }\n\n        var refreshEvents = new BraidEventStream<Unit>();\n        var app = new AppState(\n            null,\n            \"BraidLayersBinding\",\n            Minecraft.getInstance(),\n            new LayerSurface(),\n            new EventBinding.Default(),\n            new LayerContext(\n                refreshEvents.source(),\n                screen,\n                new Overlay(\n                    new Stack(widgets)\n                )\n            )\n        );\n\n        ((OwoScreenExtension) screen).owo$setBraidLayersState(new LayersState(app, refreshEvents));\n    }\n\n    // ---\n\n    public static final Identifier INIT_PHASE = Owo.id(\"init-braid-layers\");\n\n    private static final List<Layer> LAYERS = new ArrayList<>();\n\n    private record Layer(Predicate<Screen> screenPredicate, Widget widget) {}\n\n    @ApiStatus.Internal\n    public record LayersState(AppState app, BraidEventStream<Unit> refreshEvents) {}\n\n    private static class LayerSurface extends Surface.Default {\n\n        public CursorStyle currentCursorStyle = CursorStyle.NONE;\n\n        @Override\n        public void setCursorStyle(CursorStyle style) {\n            this.currentCursorStyle = style;\n        }\n\n        @Override\n        public CursorStyle currentCursorStyle() {\n            return this.currentCursorStyle;\n        }\n    }\n\n    private static final Supplier<Map<CursorStyle, CursorType>> CURSOR_MAPPINGS = Suppliers.memoize(() -> Map.of(\n        CursorStyle.POINTER, CursorTypes.ARROW,\n        CursorStyle.TEXT, CursorTypes.IBEAM,\n        CursorStyle.CROSSHAIR, CursorTypes.CROSSHAIR,\n        CursorStyle.HAND, CursorTypes.POINTING_HAND,\n        CursorStyle.VERTICAL_RESIZE, CursorTypes.RESIZE_NS,\n        CursorStyle.HORIZONTAL_RESIZE, CursorTypes.RESIZE_EW,\n        CursorStyle.MOVE, CursorTypes.RESIZE_ALL,\n        CursorStyle.NOT_ALLOWED, CursorTypes.NOT_ALLOWED\n    ));\n\n    // ---\n\n    static {\n        ScreenEvents.AFTER_INIT.addPhaseOrdering(Event.DEFAULT_PHASE, INIT_PHASE);\n        ScreenEvents.AFTER_INIT.register(INIT_PHASE, (client, screeen, scaledWidth, scaledHeight) -> {\n            if (((OwoScreenExtension)screeen).owo$getBraidLayersState() == null) {\n                setupLayers(screeen);\n            }\n\n            ScreenEvents.remove(screeen).register(screen -> {\n                var app = ((OwoScreenExtension) screen).owo$getBraidLayersApp();\n                if (app != null) {\n                    app.dispose();\n                }\n            });\n\n            ScreenMouseEvents.allowMouseClick(screeen).register((screen, click) -> {\n                return !tryHandleEvent(screen, new MouseButtonPressEvent(click.button(), click.modifiers()));\n            });\n\n            ScreenMouseEvents.allowMouseRelease(screeen).register((screen, click) -> {\n                return !tryHandleEvent(screen, new MouseButtonReleaseEvent(click.button(), click.modifiers()));\n            });\n\n            ScreenMouseEvents.allowMouseScroll(screeen).register((screen, mouseX, mouseY, horizontalAmount, verticalAmount) -> {\n                return !tryHandleEvent(screen, new MouseScrollEvent(horizontalAmount, verticalAmount));\n            });\n\n            ScreenKeyboardEvents.allowKeyPress(screeen).register((screen, keyInput) -> {\n                return !tryHandleEvent(screen, new KeyPressEvent(keyInput.key(), keyInput.scancode(), keyInput.modifiers()));\n            });\n\n            ScreenKeyboardEvents.allowKeyRelease(screeen).register((screen, keyInput) -> {\n                return !tryHandleEvent(screen, new KeyReleaseEvent(keyInput.key(), keyInput.scancode(), keyInput.modifiers()));\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/layers/Justify.java",
    "content": "package io.wispforest.owo.braid.util.layers;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class Justify extends SingleChildInstanceWidget {\n\n    public final double x;\n    public final double y;\n\n    public Justify(double x, double y, Widget child) {\n        super(child);\n        this.x = x;\n        this.y = y;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<Justify> {\n\n        public Instance(Justify widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(Justify widget) {\n            super.setWidget(widget);\n            this.justify();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            super.doLayout(constraints);\n            this.justify();\n        }\n\n        private void justify() {\n            this.child.transform.setX(-this.child.transform.width() * this.widget.x);\n            this.child.transform.setY(-this.child.transform.height() * this.widget.y);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/layers/LayerAlignment.java",
    "content": "package io.wispforest.owo.braid.util.layers;\n\nimport io.wispforest.owo.braid.core.RelativePosition;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.EmptyWidget;\nimport io.wispforest.owo.braid.widgets.overlay.Overlay;\nimport io.wispforest.owo.braid.widgets.overlay.OverlayEntry;\nimport io.wispforest.owo.braid.widgets.overlay.OverlayEntryBuilder;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\n\nimport java.util.Objects;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n// TODO:\n//  - consider whether we should provide means to render the same\n//    widget multiple times if the predicate finds more than one match\n//  - perhaps we should also make the miss behavior customizable - hide\n//    the widget when no match is found, show it when one is? keep it around\n//    once we've found a match at least once?\npublic class LayerAlignment extends StatefulWidget {\n\n    public final Function<BuildContext, @Nullable Vector2d> offsetGetter;\n    public final @Nullable AnchorJustification justification;\n    public final Widget widget;\n\n    private LayerAlignment(Function<BuildContext, @Nullable Vector2d> offsetGetter, @Nullable AnchorJustification justification, Widget widget) {\n        this.offsetGetter = offsetGetter;\n        this.justification = justification;\n        this.widget = widget;\n    }\n\n    public static LayerAlignment atVanillaWidget(Predicate<AbstractWidget> anchorPredicate, Widget widget) {\n        return atVanillaWidget(anchorPredicate, AnchorJustification.TOP_LEFT_TO_TOP_LEFT, widget);\n    }\n\n    public static LayerAlignment atVanillaWidget(Predicate<AbstractWidget> anchorPredicate, AnchorJustification justification, Widget widget) {\n        return new LayerAlignment(\n            context -> {\n                var anchor = LayerContext.findWidget(context, anchorPredicate);\n                if (anchor == null) return null;\n\n                return new Vector2d(\n                    anchor.getX() + justification.anchorX() * anchor.getWidth(),\n                    anchor.getY() + justification.anchorY() * anchor.getHeight()\n                );\n            },\n            justification,\n            widget\n        );\n    }\n\n    public static LayerAlignment atContainerScreenCoordinates(double xOffset, double yOffset, Widget widget) {\n        return new LayerAlignment(\n            context -> {\n                var root = LayerContext.containerScreenRootOf(context);\n                if (root == null) return null;\n\n                return root.add(xOffset, yOffset);\n            },\n            null,\n            widget\n        );\n    }\n\n    @Override\n    public WidgetState<LayerAlignment> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<LayerAlignment> {\n\n        private OverlayEntry entry;\n        private boolean widgetChanged = false;\n\n        @Override\n        public void init() {\n            this.scheduleOverlayUpdate();\n        }\n\n        @Override\n        public void didUpdateWidget(LayerAlignment oldWidget) {\n            this.widgetChanged = oldWidget.widget != this.widget().widget\n                || !Objects.equals(oldWidget.justification, this.widget().justification);\n        }\n\n        @Override\n        public void notifyDependenciesChanged() {\n            this.scheduleOverlayUpdate();\n        }\n\n        private void scheduleOverlayUpdate() {\n            this.schedulePostLayoutCallback(() -> {\n                var offset = this.widget().offsetGetter.apply(this.context());\n                if (offset == null) return;\n\n                if (this.entry == null) {\n                    this.entry = Overlay.of(this.context()).add(\n                        new OverlayEntryBuilder(\n                            this.prepareWidget(),\n                            new RelativePosition(this.context(), offset.x, offset.y)\n                        )\n                    );\n                } else if (this.widgetChanged || this.entry.x != offset.x || this.entry.y != offset.y) {\n                    this.widgetChanged = false;\n                    this.entry.setState(() -> {\n                        this.entry.widget = this.prepareWidget();\n                        this.entry.x = offset.x;\n                        this.entry.y = offset.y;\n                    });\n                }\n            });\n        }\n        \n        private Widget prepareWidget() {\n            var justification = this.widget().justification;\n            return justification != null\n                ? new Justify(justification.widgetX(), justification.widgetY(), this.widget().widget)\n                : this.widget().widget;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return EmptyWidget.INSTANCE;\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/util/layers/LayerContext.java",
    "content": "package io.wispforest.owo.braid.util.layers;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventSource;\nimport io.wispforest.owo.braid.widgets.eventstream.StreamListenerState;\nimport io.wispforest.owo.mixin.ui.layers.AbstractContainerScreenAccessor;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.gui.layouts.Layout;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.util.Unit;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\n\npublic class LayerContext extends StatefulWidget {\n\n    public final BraidEventSource<Unit> refreshEvents;\n    public final Screen contextScreen;\n    public final Widget child;\n\n    public LayerContext(BraidEventSource<Unit> refreshEvents, Screen contextScreen, Widget child) {\n        this.refreshEvents = refreshEvents;\n        this.contextScreen = contextScreen;\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<LayerContext> createState() {\n        return new State();\n    }\n\n    public static class State extends StreamListenerState<LayerContext> {\n\n        @Override\n        public void init() {\n            this.streamListen(widget -> widget.refreshEvents, unit -> this.setState(() -> {}));\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new LayerContextScope(\n                this.widget().child,\n                this.widget().contextScreen\n            );\n        }\n    }\n\n    // ---\n\n    private static LayerContextScope of(BuildContext context) {\n        var layerContext = context.dependOnAncestor(LayerContextScope.class);\n        if (layerContext == null) {\n            throw new IllegalStateException(\"attempted to look up the ambient LayerContext without one present\");\n        }\n\n        return layerContext;\n    }\n\n    public static AbstractWidget findWidget(BuildContext context, Predicate<AbstractWidget> predicate) {\n        var layerContext = of(context);\n\n        var widgets = new ArrayList<AbstractWidget>();\n        for (var element : layerContext.contextScreen.children()) {\n            collectChildren(element, widgets);\n        }\n\n        AbstractWidget widget = null;\n        for (var candidate : widgets) {\n            if (!predicate.test(candidate)) continue;\n            widget = candidate;\n            break;\n        }\n\n        return widget;\n    }\n\n    public static Screen screenOf(BuildContext context) {\n        return of(context).contextScreen;\n    }\n\n    public static @Nullable Vector2d containerScreenRootOf(BuildContext context) {\n        var screen = screenOf(context);\n        if (!(screen instanceof AbstractContainerScreenAccessor containerScreen)) return null;\n\n        return new Vector2d(\n            containerScreen.owo$getRootX(),\n            containerScreen.owo$getRootY()\n        );\n    }\n\n    private static void collectChildren(GuiEventListener element, List<AbstractWidget> children) {\n        if (element instanceof AbstractWidget widget) children.add(widget);\n        if (element instanceof Layout layout) {\n            layout.visitWidgets(child -> collectChildren(child, children));\n        }\n    }\n}\n\nclass LayerContextScope extends InheritedWidget {\n\n    public final Screen contextScreen;\n\n    public LayerContextScope(Widget child, Screen contextScreen) {\n        super(child);\n        this.contextScreen = contextScreen;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/BraidApp.java",
    "content": "package io.wispforest.owo.braid.widgets;\n\nimport com.google.common.collect.ImmutableMap;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.focus.FocusTraversalDirection;\nimport io.wispforest.owo.braid.widgets.intents.*;\nimport io.wispforest.owo.braid.widgets.textinput.*;\nimport net.minecraft.util.Util;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.lwjgl.glfw.GLFW.*;\n\npublic class BraidApp extends StatelessWidget {\n\n    public final Widget child;\n\n    public BraidApp(Widget child) {\n        this.child = child;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Interactable(\n            DEFAULT_SHORTCUTS,\n            widget -> widget\n                .actions(DEFAULT_ACTIONS)\n                .skipTraversal(true),\n            new Shortcuts(\n                DEFAULT_TEXT_SHORTCUTS,\n                widget -> widget\n                    .autoFocus(true)\n                    .skipTraversal(true),\n                new Navigator(\n                    this.child\n                )\n            )\n        );\n    }\n\n    // ---\n\n    private static final KeyModifiers SHIFT = new KeyModifiers(GLFW_MOD_SHIFT);\n    private static final KeyModifiers CTRL = new KeyModifiers(GLFW_MOD_CONTROL);\n    private static final KeyModifiers SHIFT_AND_CTRL = KeyModifiers.both(SHIFT, CTRL);\n\n    public static final Map<Class<? extends Intent>, Action<?>> DEFAULT_ACTIONS = Map.of(\n        TraverseFocusIntent.class, new TraverseFocusAction()\n    );\n\n    public static final Map<List<ShortcutTrigger>, Intent> DEFAULT_SHORTCUTS = Map.of(\n        List.of(new ShortcutTrigger(\n            Trigger.ofKey(GLFW_KEY_ENTER),\n            Trigger.ofKey(GLFW_KEY_KP_ENTER),\n            Trigger.ofKey(GLFW_KEY_SPACE)\n        )), PrimaryActionIntent.INSTANCE,\n        List.of(new ShortcutTrigger(\n            Trigger.ofKey(GLFW_KEY_ENTER, SHIFT),\n            Trigger.ofKey(GLFW_KEY_KP_ENTER, SHIFT),\n            Trigger.ofKey(GLFW_KEY_SPACE, SHIFT)\n        )), SecondaryActionIntent.INSTANCE,\n        List.of(ShortcutTrigger.UP.withModifiers(null)), new TraverseFocusIntent(FocusTraversalDirection.UP),\n        List.of(ShortcutTrigger.DOWN.withModifiers(null)), new TraverseFocusIntent(FocusTraversalDirection.DOWN),\n        List.of(ShortcutTrigger.LEFT.withModifiers(null)), new TraverseFocusIntent(FocusTraversalDirection.LEFT),\n        List.of(ShortcutTrigger.RIGHT.withModifiers(null)), new TraverseFocusIntent(FocusTraversalDirection.RIGHT),\n        List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_TAB))), new TraverseFocusIntent(FocusTraversalDirection.NEXT),\n        List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_TAB, SHIFT))), new TraverseFocusIntent(FocusTraversalDirection.PREVIOUS)\n    );\n\n    public static final Map<List<ShortcutTrigger>, Intent> DEFAULT_TEXT_SHORTCUTS = Util.make(() -> {\n        var builder = new ImmutableMap.Builder<List<ShortcutTrigger>, Intent>();\n\n        builder.put(List.of(new ShortcutTrigger(\n            Trigger.ofKey(GLFW_KEY_ENTER),\n            Trigger.ofKey(GLFW_KEY_KP_ENTER)\n        ).withModifiers(null)), InsertNewlineIntent.INSTANCE);\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_TAB))), InsertTabIntent.INSTANCE);\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_BACKSPACE))), new DeleteTextIntent(false, false));\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_BACKSPACE)).withModifiers(CTRL)), new DeleteTextIntent(false, true));\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_DELETE))), new DeleteTextIntent(true, false));\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_DELETE)).withModifiers(CTRL)), new DeleteTextIntent(true, true));\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_DELETE)).withModifiers(SHIFT)), DeleteLineIntent.INSTANCE);\n        builder.put(List.of(ShortcutTrigger.UP), new MoveCursorIntent(MoveCursorIntent.Direction.UP, false, false));\n        builder.put(List.of(ShortcutTrigger.DOWN), new MoveCursorIntent(MoveCursorIntent.Direction.DOWN, false, false));\n        builder.put(List.of(ShortcutTrigger.LEFT), new MoveCursorIntent(MoveCursorIntent.Direction.LEFT, false, false));\n        builder.put(List.of(ShortcutTrigger.RIGHT), new MoveCursorIntent(MoveCursorIntent.Direction.RIGHT, false, false));\n        builder.put(List.of(ShortcutTrigger.UP.withModifiers(SHIFT)), new MoveCursorIntent(MoveCursorIntent.Direction.UP, false, true));\n        builder.put(List.of(ShortcutTrigger.DOWN.withModifiers(SHIFT)), new MoveCursorIntent(MoveCursorIntent.Direction.DOWN, false, true));\n        builder.put(List.of(ShortcutTrigger.LEFT.withModifiers(SHIFT)), new MoveCursorIntent(MoveCursorIntent.Direction.LEFT, false, true));\n        builder.put(List.of(ShortcutTrigger.RIGHT.withModifiers(SHIFT)), new MoveCursorIntent(MoveCursorIntent.Direction.RIGHT, false, true));\n        builder.put(List.of(ShortcutTrigger.UP.withModifiers(CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.UP, true, false));\n        builder.put(List.of(ShortcutTrigger.DOWN.withModifiers(CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.DOWN, true, false));\n        builder.put(List.of(ShortcutTrigger.LEFT.withModifiers(CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.LEFT, true, false));\n        builder.put(List.of(ShortcutTrigger.RIGHT.withModifiers(CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.RIGHT, true, false));\n        builder.put(List.of(ShortcutTrigger.UP.withModifiers(SHIFT_AND_CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.UP, true, true));\n        builder.put(List.of(ShortcutTrigger.DOWN.withModifiers(SHIFT_AND_CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.DOWN, true, true));\n        builder.put(List.of(ShortcutTrigger.LEFT.withModifiers(SHIFT_AND_CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.LEFT, true, true));\n        builder.put(List.of(ShortcutTrigger.RIGHT.withModifiers(SHIFT_AND_CTRL)), new MoveCursorIntent(MoveCursorIntent.Direction.RIGHT, true, true));\n        builder.put(List.of(ShortcutTrigger.HOME), new TeleportCursorIntent(true, false));\n        builder.put(List.of(ShortcutTrigger.HOME.withModifiers(SHIFT)), new TeleportCursorIntent(true, true));\n        builder.put(List.of(ShortcutTrigger.END), new TeleportCursorIntent(false, false));\n        builder.put(List.of(ShortcutTrigger.END.withModifiers(SHIFT)), new TeleportCursorIntent(false, true));\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_A)).withModifiers(CTRL)), SelectAllIntent.INSTANCE);\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_C)).withModifiers(CTRL)), new CopyTextIntent(false));\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_X)).withModifiers(CTRL)), new CopyTextIntent(true));\n        builder.put(List.of(new ShortcutTrigger(Trigger.ofKey(GLFW_KEY_V)).withModifiers(CTRL)), PasteTextIntent.INSTANCE);\n\n        return builder.build();\n    });\n\n    // ---\n\n    public static class BaseRoute extends StatelessWidget {\n\n        public final Widget route;\n\n        public BaseRoute(Widget route) {\n            this.route = route;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return this.route;\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/BraidLogo.java",
    "content": "package io.wispforest.owo.braid.widgets;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.basic.TextureWidget;\nimport net.minecraft.resources.Identifier;\n\npublic class BraidLogo extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Sized(\n            64, 64,\n            new TextureWidget(\n                TEXTURE_ID,\n                TextureWidget.Wrap.STRETCH,\n                Color.WHITE\n            )\n        );\n    }\n\n    private static final Identifier TEXTURE_ID = Owo.id(\"textures/gui/braid_logo.png\");\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/Dialog.java",
    "content": "package io.wispforest.owo.braid.widgets;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.HitTestTrap;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport org.lwjgl.glfw.GLFW;\n\npublic class Dialog extends StatelessWidget {\n\n    public final Color barrierColor;\n    public final boolean barrierCanDismiss;\n    public final Widget child;\n\n    public Dialog(Color barrierColor, boolean barrierCanDismiss, Widget child) {\n        this.barrierColor = barrierColor;\n        this.barrierCanDismiss = barrierCanDismiss;\n        this.child = child;\n    }\n\n    public Dialog(Color barrierColor, Widget child) {\n        this(barrierColor, true, child);\n    }\n\n    public Dialog(boolean barrierCanDismiss, Widget child) {\n        this(DEFAULT_BARRIER_COLOR, barrierCanDismiss, child);\n    }\n\n    public Dialog(Widget child) {\n        this(DEFAULT_BARRIER_COLOR, true, child);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new HitTestTrap(\n            new MouseArea(\n                widget -> widget\n                    .clickCallback((x, y, button, modifiers) -> {\n                        if (!this.barrierCanDismiss || button != GLFW.GLFW_MOUSE_BUTTON_LEFT) return false;\n\n                        Navigator.pop(context);\n                        return true;\n                    }),\n                new Box(\n                    this.barrierColor,\n                    new Center(\n                        new HitTestTrap(\n                            this.child\n                        )\n                    )\n                )\n            )\n        );\n    }\n\n    // ---\n\n    private static final Color DEFAULT_BARRIER_COLOR = Color.BLACK.withA(.25);\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/HoverStyledLabel.java",
    "content": "package io.wispforest.owo.braid.widgets;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\n\npublic class HoverStyledLabel extends StatefulWidget {\n\n    public final Component defaultText;\n    public final Style hoverStyle;\n\n    public HoverStyledLabel(Component defaultText, Style hoverStyle) {\n        this.defaultText = defaultText;\n        this.hoverStyle = hoverStyle;\n    }\n\n    @Override\n    public WidgetState<HoverStyledLabel> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<HoverStyledLabel> {\n\n        private boolean hovered = false;\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new MouseArea(\n                widget -> widget\n                    .enterCallback(() -> setState(() -> this.hovered = true))\n                    .exitCallback(() -> setState(() -> this.hovered = false)),\n                new Label(this.hovered ? this.widget().defaultText.copy().withStyle(this.widget().hoverStyle) : this.widget().defaultText)\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/Marquee.java",
    "content": "package io.wispforest.owo.braid.widgets;\n\nimport io.wispforest.owo.braid.animation.Animation;\nimport io.wispforest.owo.braid.animation.Easing;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.Clip;\nimport io.wispforest.owo.braid.widgets.basic.ListenableBuilder;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.scroll.RawScrollView;\nimport io.wispforest.owo.braid.widgets.scroll.ScrollController;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\n\npublic class Marquee extends StatefulWidget {\n\n    protected Easing easing = Easing.IN_OUT_SINE;\n    protected Duration minDuration = Duration.ofSeconds(1);\n    protected Duration durationPerPixel = Duration.ofMillis(100);\n    protected Duration pauseTime = Duration.ofSeconds(2);\n    protected boolean pauseWhileHovered = true;\n    protected LayoutAxis axis = LayoutAxis.HORIZONTAL;\n    public final Widget child;\n\n    public Marquee(@Nullable WidgetSetupCallback<Marquee> setup, Widget child) {\n        this.child = child;\n        if (setup != null) setup.setup(this);\n    }\n\n    public Marquee(Widget child) {\n        this.child = child;\n    }\n\n    public Marquee easing(Easing easing) {\n        this.assertMutable();\n        this.easing = easing;\n        return this;\n    }\n\n    public Easing easing() {\n        return this.easing;\n    }\n\n    public Marquee minDuration(Duration minDuration) {\n        this.assertMutable();\n        this.minDuration = minDuration;\n        return this;\n    }\n\n    public Marquee minDuration(long millis) {\n        return this.minDuration(Duration.ofMillis(millis));\n    }\n\n    public Duration minDuration() {\n        return this.minDuration;\n    }\n\n    public Marquee durationPerPixel(Duration durationPerPixel) {\n        this.assertMutable();\n        this.durationPerPixel = durationPerPixel;\n        return this;\n    }\n\n    public Marquee durationPerPixel(long millisPerPixel) {\n        return this.durationPerPixel(Duration.ofMillis(millisPerPixel));\n    }\n\n    public Duration durationPerPixel() {\n        return this.durationPerPixel;\n    }\n\n    public Marquee pauseTime(Duration pauseTime) {\n        this.assertMutable();\n        this.pauseTime = pauseTime;\n        return this;\n    }\n\n    public Marquee pauseTime(long millis) {\n        return this.pauseTime(Duration.ofMillis(millis));\n    }\n\n    public Duration pauseTime() {\n        return this.pauseTime;\n    }\n\n    public Marquee pauseWhileHovered(boolean pauseWhileHovered) {\n        this.pauseWhileHovered = pauseWhileHovered;\n        return this;\n    }\n\n    public boolean pauseWhileHovered() {\n        return this.pauseWhileHovered;\n    }\n\n    public Marquee axis(LayoutAxis axis) {\n        this.assertMutable();\n        this.axis = axis;\n        return this;\n    }\n\n    public LayoutAxis axis() {\n        return this.axis;\n    }\n\n    @Override\n    public WidgetState<Marquee> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<Marquee> {\n\n        private final ScrollController controller = new ScrollController(this);\n        private Animation animation;\n        private Animation.Target pausedAnimationTarget = null;\n\n        private long callbackId = -1;\n\n        @Override\n        public void init() {\n            this.animation = new Animation(\n                this.widget().easing,\n                this.widget().durationPerPixel,\n                this::scheduleAnimationCallback,\n                this::onAnimationStep,\n                this::onAnimationFinished,\n                Animation.Target.START\n            );\n\n            this.controller.addListener(() -> {\n                this.updateAnimationDuration();\n                if (this.animation.target() == null) {\n                    this.cancelDelayedCallback(this.callbackId);\n                    this.animation.towards(this.animation.progress() == 0 ? Animation.Target.END : Animation.Target.START);\n                }\n            });\n        }\n\n        @Override\n        public void didUpdateWidget(Marquee oldWidget) {\n            super.didUpdateWidget(oldWidget);\n\n            this.updateAnimationDuration();\n            this.animation.easing = this.widget().easing;\n        }\n\n        private void updateAnimationDuration() {\n            this.animation.duration = Duration.ofNanos((long) Math.max(\n                this.widget().minDuration.toNanos(),\n                this.widget().durationPerPixel.toNanos() * this.controller.maxOffset()\n            ));\n        }\n\n        private void onAnimationStep(double progress) {\n            this.controller.jumpTo(progress * this.controller.maxOffset());\n        }\n\n        private void onAnimationFinished(Animation.Target atTarget) {\n            if (this.controller.maxOffset() == 0) return;\n\n            this.cancelDelayedCallback(this.callbackId);\n            this.callbackId = this.scheduleDelayedCallback(\n                this.widget().pauseTime,\n                () -> this.animation.towards(atTarget == Animation.Target.END ? Animation.Target.START : Animation.Target.END)\n            );\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new MouseArea(\n                widget -> {\n                    if (!this.widget().pauseWhileHovered) return;\n                    widget\n                        .enterCallback(() -> {\n                            this.pausedAnimationTarget = this.animation.target();\n                            this.animation.pause();\n                        })\n                        .exitCallback(() -> {\n                            if (this.pausedAnimationTarget == null) return;\n                            this.animation.towards(this.pausedAnimationTarget, false);\n                        });\n                },\n                new Clip(\n                    new ListenableBuilder(\n                        this.controller,\n                        buildContext -> new RawScrollView(\n                            this.widget().axis == LayoutAxis.HORIZONTAL ? this.controller : null,\n                            this.widget().axis == LayoutAxis.VERTICAL ? this.controller : null,\n                            this.widget().child\n                        )\n                    )\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/Navigator.java",
    "content": "package io.wispforest.owo.braid.widgets;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.sharedstate.ShareableState;\nimport io.wispforest.owo.braid.widgets.sharedstate.SharedState;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Navigator extends StatelessWidget {\n\n    public final @Nullable Widget initialRoute;\n\n    public Navigator(@Nullable Widget initialRoute) {\n        this.initialRoute = initialRoute;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new SharedState<>(\n            () -> new NavigationState(this.initialRoute),\n            new Builder(innerContext -> {\n                var state = SharedState.get(innerContext, NavigationState.class);\n                return new Stack(state.displayedRoutes());\n            })\n        );\n    }\n\n    // ---\n\n    public static void pushOverlay(BuildContext context, Widget route) {\n        SharedState.set(context, NavigationState.class, state -> state.push(route, true));\n    }\n\n    public static void push(BuildContext context, Widget route) {\n        SharedState.set(context, NavigationState.class, state -> state.push(route, false));\n    }\n\n    public static void pop(BuildContext context) {\n        SharedState.set(context, NavigationState.class, NavigationState::pop);\n    }\n}\n\nrecord Route(Widget widget, boolean overlay) {}\n\nclass NavigationState extends ShareableState {\n    private final List<Route> routes;\n    private List<Widget> displayedRoutes = List.of();\n\n    public NavigationState(@Nullable Widget initialRoute) {\n        this.routes = initialRoute != null ? new ArrayList<>(List.of(new Route(initialRoute, false))) : new ArrayList<>();\n        this.updateDisplayedRoutes();\n    }\n\n    public List<Widget> displayedRoutes() {\n        return this.displayedRoutes;\n    }\n\n    public void push(Widget route, boolean overlay) {\n        this.routes.add(new Route(route, overlay));\n        this.updateDisplayedRoutes();\n    }\n\n    public void pop() {\n        this.routes.removeLast();\n        this.updateDisplayedRoutes();\n    }\n\n    private void updateDisplayedRoutes() {\n        int idx;\n        for (idx = this.routes.size() - 1; idx >= 0; idx--) {\n            if (!this.routes.get(idx).overlay()) {\n                break;\n            }\n        }\n\n        this.displayedRoutes = this.routes.subList(idx, this.routes.size()).stream().map(Route::widget).toList();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/SpriteWidget.java",
    "content": "package io.wispforest.owo.braid.widgets;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.client.renderer.texture.TextureAtlasSprite;\nimport net.minecraft.client.renderer.texture.TextureManager;\nimport net.minecraft.client.resources.model.Material;\nimport net.minecraft.resources.Identifier;\n\nimport java.util.OptionalDouble;\n\npublic class SpriteWidget extends LeafInstanceWidget {\n\n    public static final Identifier GUI_ATLAS_ID = Identifier.withDefaultNamespace(\"textures/atlas/gui.png\");\n\n    public final Material spriteIdentifier;\n\n    public SpriteWidget(Material spriteIdentifier) {\n        this.spriteIdentifier = spriteIdentifier;\n    }\n\n    public SpriteWidget(Identifier spriteIdentifier) {\n        this.spriteIdentifier = new Material(GUI_ATLAS_ID, spriteIdentifier);\n    }\n\n    @Override\n    public LeafWidgetInstance<SpriteWidget> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends LeafWidgetInstance<SpriteWidget> {\n\n        protected TextureAtlasSprite sprite;\n\n        public Instance(SpriteWidget widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(SpriteWidget widget) {\n            if (this.widget.spriteIdentifier.equals(widget.spriteIdentifier)) return;\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        protected TextureAtlasSprite findSprite() {\n            try {\n                this.sprite = Minecraft.getInstance().getAtlasManager().get(this.widget.spriteIdentifier);\n            } catch (IllegalArgumentException ignored) {\n                this.sprite = Minecraft.getInstance().getAtlasManager().get(new Material(GUI_ATLAS_ID, TextureManager.INTENTIONAL_MISSING_TEXTURE));\n            }\n\n            return this.sprite;\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            this.sprite = this.findSprite();\n\n            var size = Size.of(\n                this.sprite.contents().width(),\n                this.sprite.contents().height()\n            ).constrained(constraints);\n\n            this.transform.setSize(size);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.findSprite().contents().width();\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.findSprite().contents().height();\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            graphics.blitSprite(\n                RenderPipelines.GUI_TEXTURED,\n                this.sprite,\n                0,\n                0,\n                (int) this.transform.width(),\n                (int) this.transform.height()\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/animated/AnimatedAlign.java",
    "content": "package io.wispforest.owo.braid.widgets.animated;\n\nimport io.wispforest.owo.braid.animation.AlignmentLerp;\nimport io.wispforest.owo.braid.animation.AutomaticallyAnimatedWidget;\nimport io.wispforest.owo.braid.animation.Easing;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Align;\n\nimport java.time.Duration;\n\npublic class AnimatedAlign extends AutomaticallyAnimatedWidget {\n\n    public final Alignment alignment;\n    public final Widget child;\n\n    public AnimatedAlign(Duration duration, Easing easing, Alignment alignment, Widget child) {\n        super(duration, easing);\n        this.alignment = alignment;\n        this.child = child;\n    }\n\n    @Override\n    public State createState() {\n        return new State();\n    }\n\n    public static class State extends AutomaticallyAnimatedWidget.State<AnimatedAlign> {\n\n        private AlignmentLerp alignment;\n\n        @Override\n        protected void updateLerps() {\n            this.alignment = this.visitLerp(this.alignment, this.widget().alignment, AlignmentLerp::new);\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Align(\n                this.alignment.compute(this.animationValue()),\n                this.widget().child\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/animated/AnimatedBox.java",
    "content": "package io.wispforest.owo.braid.widgets.animated;\n\nimport io.wispforest.owo.braid.animation.AutomaticallyAnimatedWidget;\nimport io.wispforest.owo.braid.animation.ColorLerp;\nimport io.wispforest.owo.braid.animation.Easing;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\n\npublic class AnimatedBox extends AutomaticallyAnimatedWidget {\n\n    public final Color color;\n    public final boolean outline;\n    public final @Nullable Widget child;\n\n    public AnimatedBox(Duration duration, Easing easing, Color color, boolean outline, @Nullable Widget child) {\n        super(duration, easing);\n        this.color = color;\n        this.outline = outline;\n        this.child = child;\n    }\n\n    public AnimatedBox(Duration duration, Easing easing, Color color, boolean outline) {\n        this(duration, easing, color, outline, null);\n    }\n\n    public AnimatedBox(Duration duration, Easing easing, Color color) {\n        this(duration, easing, color, false);\n    }\n\n    @Override\n    public State createState() {\n        return new State();\n    }\n\n    public static class State extends AutomaticallyAnimatedWidget.State<AnimatedBox> {\n\n        private ColorLerp color;\n\n        @Override\n        protected void updateLerps() {\n            this.color = this.visitLerp(this.color, this.widget().color, ColorLerp::new);\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Box(\n                this.color.compute(this.animationValue()),\n                this.widget().outline,\n                this.widget().child\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/animated/AnimatedPadding.java",
    "content": "package io.wispforest.owo.braid.widgets.animated;\n\nimport io.wispforest.owo.braid.animation.AutomaticallyAnimatedWidget;\nimport io.wispforest.owo.braid.animation.Easing;\nimport io.wispforest.owo.braid.animation.InsetsLerp;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\n\npublic class AnimatedPadding extends AutomaticallyAnimatedWidget {\n    public final Insets insets;\n    public final @Nullable Widget child;\n\n    public AnimatedPadding(Duration duration, Easing easing, Insets insets, @Nullable Widget child) {\n        super(duration, easing);\n        this.insets = insets;\n        this.child = child;\n    }\n\n    @Override\n    public State createState() {\n        return new State();\n    }\n\n    public static class State extends AutomaticallyAnimatedWidget.State<AnimatedPadding> {\n\n        private InsetsLerp insets;\n\n        @Override\n        protected void updateLerps() {\n            this.insets = this.visitLerp(this.insets, this.widget().insets, InsetsLerp::new);\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Padding(\n                this.insets.compute(this.animationValue()),\n                this.widget().child\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/animated/AnimatedSized.java",
    "content": "package io.wispforest.owo.braid.widgets.animated;\n\nimport io.wispforest.owo.braid.animation.AutomaticallyAnimatedWidget;\nimport io.wispforest.owo.braid.animation.DoubleLerp;\nimport io.wispforest.owo.braid.animation.Easing;\nimport io.wispforest.owo.braid.animation.Lerp;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\n\npublic class AnimatedSized extends AutomaticallyAnimatedWidget {\n\n    public final @Nullable Double width;\n    public final @Nullable Double height;\n    public final Widget child;\n\n    public AnimatedSized(Duration duration, Easing easing, @Nullable Double width, @Nullable Double height, Widget child) {\n        super(duration, easing);\n        this.width = width;\n        this.height = height;\n        this.child = child;\n    }\n\n    @Override\n    public State createState() {\n        return new State();\n    }\n\n    public static class State extends AutomaticallyAnimatedWidget.State<AnimatedSized> {\n\n        private Lerp<@Nullable Double> width;\n        private Lerp<@Nullable Double> height;\n\n        @Override\n        protected void updateLerps() {\n            this.width = this.visitNullableLerp(this.width, this.widget().width, DoubleLerp::new);\n            this.height = this.visitNullableLerp(this.height, this.widget().height, DoubleLerp::new);\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Sized(\n                this.width.compute(this.animationValue()),\n                this.height.compute(this.animationValue()),\n                this.widget().child\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Align.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\nimport java.util.OptionalDouble;\n\npublic class Align extends SingleChildInstanceWidget {\n\n    public final Alignment alignment;\n    public final OptionalDouble widthFactor;\n    public final OptionalDouble heightFactor;\n\n    public Align(Alignment alignment, @Nullable Double widthFactor, @Nullable Double heightFactor, Widget child) {\n        super(child);\n        this.alignment = alignment;\n        this.widthFactor = widthFactor != null ? OptionalDouble.of(widthFactor) : OptionalDouble.empty();\n        this.heightFactor = heightFactor != null ? OptionalDouble.of(heightFactor) : OptionalDouble.empty();\n    }\n\n    public Align(Alignment alignment, Widget child) {\n        this(alignment, null, null, child);\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance<Align> {\n\n        public Instance(Align widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(Align widget) {\n            if (Objects.equals(this.widget.widthFactor, widget.widthFactor)\n                && Objects.equals(this.widget.heightFactor, widget.heightFactor)\n                && Objects.equals(this.widget.alignment, widget.alignment)) {\n                return;\n            }\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var widthFactor = this.widget.widthFactor;\n            var heightFactor = this.widget.heightFactor;\n            var alignment = this.widget.alignment;\n\n            var childSize = this.child.layout(constraints.asLoose());\n            var selfSize = Size.of(\n                widthFactor.isPresent() || !constraints.hasBoundedWidth() ? childSize.width() * widthFactor.orElse(1) : constraints.maxWidth(),\n                heightFactor.isPresent() || !constraints.hasBoundedHeight()\n                    ? childSize.height() * heightFactor.orElse(1)\n                    : constraints.maxHeight()\n            ).constrained(constraints);\n\n            var childX = alignment.alignHorizontal(selfSize.width(), childSize.width());\n            var childY = alignment.alignVertical(selfSize.height(), childSize.height());\n            this.child.transform.setX(childX);\n            this.child.transform.setY(childY);\n\n            this.transform.setSize(selfSize);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.child.getIntrinsicWidth(height) * (this.widget.widthFactor.orElse(1));\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.child.getIntrinsicHeight(width) * (this.widget.heightFactor.orElse(1));\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child.getBaselineOffset().stream().map(operand -> operand + this.child.transform.y()).findAny();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/AspectRatio.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.OptionalDouble;\n\npublic class AspectRatio extends SingleChildInstanceWidget {\n\n    public final double ratio;\n\n    public AspectRatio(double ratio, Widget child) {\n        super(child);\n        this.ratio = ratio;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    // ---\n\n    public static Size applyAspectRatioToMaxSize(Constraints constraints, double ratio) {\n        double width = constraints.maxWidth();\n        double height;\n\n        if (Double.isFinite(width)) {\n            height = width / ratio;\n        } else {\n            height = constraints.maxHeight();\n            width = height * ratio;\n        }\n\n        return applyAspectRatio(constraints, Size.of(width, height));\n    }\n\n    public static Size applyAspectRatio(Constraints constraints, Size size) {\n        if (constraints.isTight()) {\n            return constraints.minSize();\n        }\n\n        var width = size.width();\n        var height = size.height();\n        var ratio = width / height;\n\n        if (width > constraints.maxWidth()) {\n            width = constraints.maxWidth();\n            height = width / ratio;\n        }\n\n        if (height > constraints.maxHeight()) {\n            height = constraints.maxHeight();\n            width = height * ratio;\n        }\n\n        if (width < constraints.minWidth()) {\n            width = constraints.minWidth();\n            height = width / ratio;\n        }\n\n        if (height < constraints.minHeight()) {\n            height = constraints.minHeight();\n            width = height * ratio;\n        }\n\n        return Size.of(width, height).constrained(constraints);\n    }\n\n    // ---\n\n    public static class Instance extends SingleChildWidgetInstance<AspectRatio> {\n\n        public Instance(AspectRatio widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(AspectRatio widget) {\n            if (widget.ratio == this.widget.ratio) return;\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var size = AspectRatio.applyAspectRatioToMaxSize(constraints, this.widget.ratio);\n            this.transform.setSize(size);\n\n            this.child.layout(Constraints.tight(size));\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return Double.isFinite(height) ? height * this.widget.ratio : this.child.getIntrinsicWidth(height);\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return Double.isFinite(width) ? width / this.widget.ratio : this.child.getIntrinsicHeight(width);\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child.getBaselineOffset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Blur.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.ui.renderstate.BlurQuadElementRenderState;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport org.joml.Matrix3x2f;\n\npublic class Blur extends SingleChildInstanceWidget {\n\n    public final float quality;\n    public final float size;\n    public final boolean blurChild;\n\n    public Blur(float quality, float size, boolean blurChild, Widget child) {\n        super(child);\n        this.quality = quality;\n        this.size = size;\n        this.blurChild = blurChild;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<Blur> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<Blur> {\n\n        public Instance(Blur widget) {\n            super(widget);\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            if (!this.widget.blurChild) {\n                this.drawBlur(graphics);\n            }\n\n            super.draw(graphics);\n\n            if (this.widget.blurChild) {\n                this.drawBlur(graphics);\n            }\n        }\n\n        private void drawBlur(BraidGraphics ctx) {\n            ctx.guiRenderState.submitGuiElement(new BlurQuadElementRenderState(\n                new Matrix3x2f(ctx.pose()),\n                new ScreenRectangle(0, 0, (int) this.transform.width(), (int) this.transform.height()),\n                ctx.scissorStack.peek(),\n                16, this.widget.quality, this.widget.size\n            ));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Box.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.OptionalChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Box extends OptionalChildInstanceWidget {\n\n    public final Color color;\n    public final boolean outline;\n\n    public Box(Color color, boolean outline, @Nullable Widget child) {\n        super(child);\n        this.color = color;\n        this.outline = outline;\n    }\n\n    public Box(Color color, @Nullable Widget child) {\n        this(color, false, child);\n    }\n\n    public Box(Color color, boolean outline) {\n        this(color, outline, null);\n    }\n\n    public Box(Color color) {\n        this(color, false);\n    }\n\n    @Override\n    public OptionalChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends OptionalChildWidgetInstance.ShrinkWrap<Box> {\n\n        public Instance(Box widget) {\n            super(widget);\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            if (this.widget.outline) {\n                graphics.drawRectOutline(0, 0, (int) this.transform.width(), (int) this.transform.height(), this.widget.color.argb());\n            } else {\n                graphics.fill(0, 0, (int) this.transform.width(), (int) this.transform.height(), this.widget.color.argb());\n            }\n\n            super.draw(graphics);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Builder.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class Builder extends StatelessWidget {\n    public final WidgetBuilder builder;\n\n    public Builder(WidgetBuilder builder) {\n        this.builder = builder;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return this.builder.build(context);\n    }\n\n    @FunctionalInterface\n    public interface WidgetBuilder {\n        Widget build(BuildContext context);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Center.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Center extends Align {\n    public Center(@Nullable Double widthFactor, @Nullable Double heightFactor, Widget child) {\n        super(Alignment.CENTER, widthFactor, heightFactor, child);\n    }\n\n    public Center(Widget child) {\n        this(null, null, child);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Clip.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.framework.instance.HitTestState;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\n\n// TODO: stencil clip\n//  also warn in docs about transforms which aren't pure translations\npublic class Clip extends SingleChildInstanceWidget {\n\n    public final boolean clipHitTest;\n    public final boolean clipDrawing;\n\n    public Clip(boolean clipHitTest, boolean clipDrawing, Widget child) {\n        super(child);\n        this.clipHitTest = clipHitTest;\n        this.clipDrawing = clipDrawing;\n    }\n\n    public Clip(Widget child) {\n        this(true, true, child);\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<Clip> {\n\n        public Instance(Clip widget) {\n            super(widget);\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            if (!this.widget.clipDrawing) {\n                super.draw(graphics);\n                return;\n            }\n\n            graphics.scissorStack.push(new ScreenRectangle(0, 0, (int) this.transform.width(), (int) this.transform.height()).transformMaxBounds(graphics.pose()));\n            super.draw(graphics);\n            graphics.disableScissor();\n        }\n\n        @Override\n        public void hitTest(double x, double y, HitTestState state) {\n            if (this.widget.clipHitTest && (x < 0 || x > this.transform.width() || y < 0 || y > this.transform.height())) {\n                return;\n            }\n\n            super.hitTest(x, y, state);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Constrain.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class Constrain extends ConstraintWidget {\n\n    public final Constraints constraints;\n\n    public Constrain(Constraints constraints, Widget child) {\n        super(child);\n        this.constraints = constraints;\n    }\n\n    @Override\n    protected Constraints constraints() {\n        return this.constraints;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/ConstraintWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.util.Mth;\n\nimport java.util.Objects;\nimport java.util.OptionalDouble;\n\npublic abstract class ConstraintWidget extends SingleChildInstanceWidget {\n\n    protected ConstraintWidget(Widget child) {\n        super(child);\n    }\n\n    protected abstract Constraints constraints();\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance<ConstraintWidget> {\n\n        public Instance(ConstraintWidget widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(ConstraintWidget widget) {\n            if (Objects.equals(this.widget.constraints(), widget.constraints())) {\n                return;\n            }\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            this.sizeToChild(this.widget.constraints().respecting(constraints), this.child);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return Mth.clamp(this.child.getIntrinsicWidth(height), this.widget.constraints().minWidth(), this.widget.constraints().maxWidth());\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return Mth.clamp(this.child.getIntrinsicHeight(width), this.widget.constraints().minHeight(), this.widget.constraints().maxHeight());\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child.getBaselineOffset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/ControlsOverride.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\n/// A widget that descendants can check to disable interactive controls,\n/// such as buttons or text fields.\n///\n/// This is useful for deactivating larger sections of a UI\n/// without having to manually disable each individual widget.\npublic class ControlsOverride extends InheritedWidget {\n\n    public final boolean disableControls;\n\n    public ControlsOverride(boolean disableControls, Widget child) {\n        super(child);\n        this.disableControls = disableControls;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return this.disableControls != ((ControlsOverride) newWidget).disableControls;\n    }\n\n    public static boolean controlsDisabled(BuildContext context) {\n        var widget = context.dependOnAncestor(ControlsOverride.class);\n        return widget != null && widget.disableControls;\n    }\n}\n\n//TODO: make sure this is applied to all relevant widgets\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/CustomDraw.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetTransform;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\n\nimport java.util.OptionalDouble;\n\npublic class CustomDraw extends LeafInstanceWidget {\n\n    public final CustomDrawFunction function;\n\n    public CustomDraw(CustomDrawFunction function) {\n        this.function = function;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    @FunctionalInterface\n    public interface CustomDrawFunction {\n        void draw(BraidGraphics graphics, WidgetTransform transform);\n    }\n\n    public static class Instance extends LeafWidgetInstance<CustomDraw> {\n\n        public Instance(CustomDraw widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var size = constraints.minSize();\n            this.transform.setSize(size);\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            this.widget.function.draw(graphics, this.transform);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return 0;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return 0;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/EmptyWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class EmptyWidget extends StatelessWidget {\n\n    public static final EmptyWidget INSTANCE = new EmptyWidget();\n\n    private EmptyWidget() {}\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Padding(Insets.none());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/HitTestTrap.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class HitTestTrap extends VisitorWidget {\n\n    public final boolean occludeHitTest;\n\n    public HitTestTrap(boolean occludeHitTest, Widget child) {\n        super(child);\n        this.occludeHitTest = occludeHitTest;\n    }\n\n    public HitTestTrap(Widget child) {\n        this(true, child);\n    }\n\n    public static final Visitor<HitTestTrap> VISITOR = (widget, instance) -> {\n        if (widget.occludeHitTest) {\n            instance.flags |= WidgetInstance.FLAG_HIT_TEST_BOUNDARY;\n        } else {\n            instance.flags &= ~WidgetInstance.FLAG_HIT_TEST_BOUNDARY;\n        }\n    };\n\n    @Override\n    public Proxy<?> proxy() {\n        return new Proxy<>(this, VISITOR);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/HoverableBuilder.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\npublic class HoverableBuilder extends StatefulWidget {\n    public final HoverableBuilderCallback builder;\n    public final @Nullable Widget child;\n\n    public HoverableBuilder(HoverableBuilderCallback builder, @NotNull Widget child) {\n        this.builder = builder;\n        this.child = child;\n    }\n\n    public HoverableBuilder(HoverableBuilderCallbackWithoutChild builder) {\n        this.builder = (context, hovered, $) -> builder.build(context, hovered);\n        this.child = null;\n    }\n\n    public HoverableBuilder(Widget notHovered, Widget hovered) {\n        this((context, isHovered) -> isHovered ? hovered : notHovered);\n    }\n\n    @Override\n    public WidgetState<HoverableBuilder> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<HoverableBuilder> {\n\n        private boolean hovered = false;\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new MouseArea(\n                widget -> widget\n                    .enterCallback(() -> this.setState(() -> this.hovered = true))\n                    .exitCallback(() -> this.setState(() -> this.hovered = false)),\n                this.widget().builder.build(context, this.hovered, this.widget().child)\n            );\n        }\n    }\n\n    @FunctionalInterface\n    public interface HoverableBuilderCallback {\n        Widget build(BuildContext hoverableContext, boolean hovered, Widget child);\n    }\n\n    @FunctionalInterface\n    public interface HoverableBuilderCallbackWithoutChild {\n        Widget build(BuildContext hoverableContext, boolean hovered);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/IntrinsicHeight.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.OptionalDouble;\n\npublic class IntrinsicHeight extends SingleChildInstanceWidget {\n\n    public IntrinsicHeight(Widget child) {\n        super(child);\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance<IntrinsicHeight> {\n\n        public Instance(IntrinsicHeight widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var childSize = this.child.getIntrinsicHeight(constraints.maxWidth());\n\n            var childConstraints = Constraints.of(\n                constraints.minWidth(),\n                childSize,\n                constraints.maxWidth(),\n                childSize\n            ).respecting(constraints);\n\n            this.transform.setSize(this.child.layout(childConstraints));\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.child.getIntrinsicWidth(height);\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.child.getIntrinsicHeight(width);\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child.getBaselineOffset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/IntrinsicWidth.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.OptionalDouble;\n\npublic class IntrinsicWidth extends SingleChildInstanceWidget {\n\n    public IntrinsicWidth(Widget child) {\n        super(child);\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance<IntrinsicWidth> {\n\n        public Instance(IntrinsicWidth widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var childSize = this.child.getIntrinsicWidth(constraints.maxHeight());\n\n            var childConstraints = Constraints.of(\n                childSize,\n                constraints.minHeight(),\n                childSize,\n                constraints.maxHeight()\n            ).respecting(constraints);\n\n            this.transform.setSize(this.child.layout(childConstraints));\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.child.getIntrinsicWidth(height);\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.child.getIntrinsicHeight(width);\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child.getBaselineOffset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/LayoutBuilder.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.BuildScope;\nimport io.wispforest.owo.braid.framework.proxy.InstanceWidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.framework.widget.InstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.Consumer;\n\npublic class LayoutBuilder extends InstanceWidget {\n\n    public final Callback builder;\n\n    public LayoutBuilder(Callback builder) {\n        this.builder = builder;\n    }\n\n    @Override\n    public WidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    @Override\n    public WidgetProxy proxy() {\n        return new Proxy(this);\n    }\n\n    public static class Proxy extends InstanceWidgetProxy {\n\n        protected final BuildScope scope = new BuildScope(() -> {\n            this.instance.markNeedsLayout();\n        });\n        protected WidgetProxy child;\n\n        protected Proxy(InstanceWidget widget) {\n            super(widget);\n            this.instance().callback = this::rebuild;\n        }\n\n        @Override\n        public Instance instance() {\n            return (Instance) super.instance();\n        }\n\n        @Override\n        public BuildScope buildScope() {\n            return this.scope;\n        }\n\n        @Override\n        public void updateWidget(Widget newWidget) {\n            super.updateWidget(newWidget);\n            this.instance.markNeedsLayout();\n        }\n\n        protected void rebuild(Constraints constraints) {\n            var newWidget = ((LayoutBuilder) this.widget()).builder.build(this, constraints);\n            this.child = this.refreshChild(this.child, newWidget, null);\n\n            this.buildScope().rebuildDirtyProxies();\n        }\n\n        @Override\n        public void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot) {\n            this.instance().setChild(instance);\n        }\n\n        @Override\n        public void visitChildren(Visitor visitor) {\n            if (this.child != null) {\n                visitor.visit(this.child);\n            }\n        }\n    }\n\n    public static class Instance extends OptionalChildWidgetInstance.ShrinkWrap<LayoutBuilder> {\n\n        private Consumer<Constraints> callback;\n\n        public Instance(LayoutBuilder widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            this.host().notifySubtreeRebuild();\n            this.callback.accept(constraints);\n\n            super.doLayout(constraints);\n        }\n    }\n\n    @FunctionalInterface\n    public interface Callback {\n        Widget build(BuildContext context, Constraints constraints);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/ListenableBuilder.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Listenable;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\npublic class ListenableBuilder extends StatefulWidget {\n\n    public final Listenable listenable;\n    public final ListenableBuilderWithChildFunction builder;\n    public final @Nullable Widget child;\n\n    public ListenableBuilder(Listenable listenable, ListenableBuilderFunction builder) {\n        this.listenable = listenable;\n        this.builder = (context, $) -> builder.build(context);\n        this.child = null;\n    }\n\n    public ListenableBuilder(Listenable listenable, ListenableBuilderWithChildFunction builder, @NotNull Widget child) {\n        this.listenable = listenable;\n        this.builder = builder;\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<ListenableBuilder> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<ListenableBuilder> {\n\n        private final Runnable listener = () -> this.setState(() -> {});\n\n        @Override\n        public void init() {\n            this.widget().listenable.addListener(this.listener);\n        }\n\n        @Override\n        public void didUpdateWidget(ListenableBuilder oldWidget) {\n            if (this.widget().listenable != oldWidget.listenable) {\n                oldWidget.listenable.removeListener(this.listener);\n                this.widget().listenable.addListener(this.listener);\n            }\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return this.widget().builder.build(context, this.widget().child);\n        }\n\n        @Override\n        public void dispose() {\n            this.widget().listenable.removeListener(this.listener);\n        }\n    }\n\n    // ---\n\n    @FunctionalInterface\n    public interface ListenableBuilderFunction {\n        Widget build(BuildContext listenableContext);\n    }\n\n    @FunctionalInterface\n    public interface ListenableBuilderWithChildFunction {\n        Widget build(BuildContext listenableContext, Widget child);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/MouseArea.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.instance.MouseListener;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport org.jetbrains.annotations.Nullable;\n\npublic class MouseArea extends SingleChildInstanceWidget {\n\n    private @Nullable ClickCallback clickCallback;\n    private @Nullable ReleaseCallback releaseCallback;\n    private @Nullable EnterCallback enterCallback;\n    private @Nullable MoveCallback moveCallback;\n    private @Nullable ExitCallback exitCallback;\n    private @Nullable DragStartCallback dragStartCallback;\n    private @Nullable DragCallback dragCallback;\n    private @Nullable DragEndCallback dragEndCallback;\n    private @Nullable ScrollCallback scrollCallback;\n    private @Nullable CursorStyleSupplier cursorStyleSupplier;\n\n    public MouseArea(\n        WidgetSetupCallback<MouseArea> setupCallback,\n        Widget child\n    ) {\n        super(child);\n        setupCallback.setup(this);\n    }\n\n    public MouseArea clickCallback(@Nullable ClickCallback clickCallback) {\n        this.assertMutable();\n        this.clickCallback = clickCallback;\n        return this;\n    }\n\n    public @Nullable ClickCallback clickCallback() {\n        return this.clickCallback;\n    }\n\n    public MouseArea releaseCallback(@Nullable ReleaseCallback releaseCallback) {\n        this.assertMutable();\n        this.releaseCallback = releaseCallback;\n        return this;\n    }\n\n    public @Nullable ReleaseCallback releaseCallback() {\n        return this.releaseCallback;\n    }\n\n    public MouseArea enterCallback(@Nullable EnterCallback enterCallback) {\n        this.assertMutable();\n        this.enterCallback = enterCallback;\n        return this;\n    }\n\n    public @Nullable EnterCallback enterCallback() {\n        return this.enterCallback;\n    }\n\n    public MouseArea moveCallback(@Nullable MoveCallback moveCallback) {\n        this.assertMutable();\n        this.moveCallback = moveCallback;\n        return this;\n    }\n\n    public @Nullable MoveCallback moveCallback() {\n        return this.moveCallback;\n    }\n\n    public MouseArea exitCallback(@Nullable ExitCallback exitCallback) {\n        this.assertMutable();\n        this.exitCallback = exitCallback;\n        return this;\n    }\n\n    public @Nullable ExitCallback exitCallback() {\n        return this.exitCallback;\n    }\n\n    public MouseArea dragStartCallback(@Nullable DragStartCallback dragStartCallback) {\n        this.assertMutable();\n        this.dragStartCallback = dragStartCallback;\n        return this;\n    }\n\n    public @Nullable DragStartCallback dragStartCallback() {\n        return this.dragStartCallback;\n    }\n\n    public MouseArea dragCallback(@Nullable DragCallback dragCallback) {\n        this.assertMutable();\n        this.dragCallback = dragCallback;\n        return this;\n    }\n\n    public @Nullable DragCallback dragCallback() {\n        return this.dragCallback;\n    }\n\n    public MouseArea dragEndCallback(@Nullable DragEndCallback dragEndCallback) {\n        this.assertMutable();\n        this.dragEndCallback = dragEndCallback;\n        return this;\n    }\n\n    public @Nullable DragEndCallback dragEndCallback() {\n        return this.dragEndCallback;\n    }\n\n    public MouseArea scrollCallback(@Nullable ScrollCallback scrollCallback) {\n        this.assertMutable();\n        this.scrollCallback = scrollCallback;\n        return this;\n    }\n\n    public @Nullable ScrollCallback scrollCallback() {\n        return this.scrollCallback;\n    }\n\n    public MouseArea cursorStyleSupplier(@Nullable CursorStyleSupplier cursorStyleSupplier) {\n        this.assertMutable();\n        this.cursorStyleSupplier = cursorStyleSupplier;\n        return this;\n    }\n\n    public MouseArea cursorStyle(@Nullable CursorStyle style) {\n        return this.cursorStyleSupplier((x, y) -> style);\n    }\n\n    public @Nullable CursorStyleSupplier cursorStyleSupplier() {\n        return this.cursorStyleSupplier;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    @FunctionalInterface\n    public interface ClickCallback {\n        boolean onClick(double x, double y, int button, KeyModifiers modifiers);\n    }\n\n    @FunctionalInterface\n    public interface ReleaseCallback {\n        boolean onRelease(double x, double y, int button, KeyModifiers modifiers);\n    }\n\n    @FunctionalInterface\n    public interface EnterCallback {\n        void onMouseEnter();\n    }\n\n    @FunctionalInterface\n    public interface MoveCallback {\n        void onMouseMove(double toX, double toY);\n    }\n\n    @FunctionalInterface\n    public interface ExitCallback {\n        void onMouseExit();\n    }\n\n    @FunctionalInterface\n    public interface DragStartCallback {\n        void onDragStart(int button, KeyModifiers modifiers);\n    }\n\n    @FunctionalInterface\n    public interface DragCallback {\n        void onDrag(double x, double y, double dx, double dy);\n    }\n\n    @FunctionalInterface\n    public interface DragEndCallback {\n        void onDragEnd();\n    }\n\n    @FunctionalInterface\n    public interface ScrollCallback {\n        boolean onScroll(double horizontal, double vertical);\n    }\n\n    @FunctionalInterface\n    public interface CursorStyleSupplier {\n        @Nullable CursorStyle getCursorStyle(double x, double y);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<MouseArea> implements MouseListener {\n\n        public Instance(MouseArea widget) {\n            super(widget);\n        }\n\n        @Override\n        public @Nullable CursorStyle cursorStyleAt(double x, double y) {\n            if (this.widget.cursorStyleSupplier == null) return null;\n            return this.widget.cursorStyleSupplier.getCursorStyle(x, y);\n        }\n\n        @Override\n        public boolean onMouseDown(double x, double y, int button, KeyModifiers modifiers) {\n            if (this.widget.clickCallback != null) {\n                return this.widget.clickCallback.onClick(x, y, button, modifiers);\n            }\n\n            return this.widget.dragCallback != null;\n        }\n\n        @Override\n        public boolean onMouseUp(double x, double y, int button, KeyModifiers modifiers) {\n            if (this.widget.releaseCallback != null) {\n                return this.widget.releaseCallback.onRelease(x, y, button, modifiers);\n            }\n\n            return this.widget.dragEndCallback != null;\n        }\n\n        @Override\n        public void onMouseEnter() {\n            if (this.widget.enterCallback != null) this.widget.enterCallback.onMouseEnter();\n        }\n\n        @Override\n        public void onMouseMove(double toX, double toY) {\n            if (this.widget.moveCallback != null) this.widget.moveCallback.onMouseMove(toX, toY);\n        }\n\n        @Override\n        public void onMouseExit() {\n            if (this.widget.exitCallback != null) this.widget.exitCallback.onMouseExit();\n        }\n\n        @Override\n        public void onMouseDragStart(int button, KeyModifiers modifiers) {\n            if (this.widget.dragStartCallback != null) this.widget.dragStartCallback.onDragStart(button, modifiers);\n        }\n\n        @Override\n        public void onMouseDrag(double x, double y, double dx, double dy) {\n            if (this.widget.dragCallback != null) this.widget.dragCallback.onDrag(x, y, dx, dy);\n        }\n\n        @Override\n        public void onMouseDragEnd() {\n            if (this.widget.dragEndCallback != null) this.widget.dragEndCallback.onDragEnd();\n        }\n\n        @Override\n        public boolean onMouseScroll(double x, double y, double horizontal, double vertical) {\n            if (this.widget.scrollCallback != null) {\n                return this.widget.scrollCallback.onScroll(horizontal, vertical);\n            }\n\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Padding.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.OptionalChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\nimport java.util.OptionalDouble;\n\npublic class Padding extends OptionalChildInstanceWidget {\n\n    public final Insets insets;\n\n    public Padding(Insets insets, @Nullable Widget child) {\n        super(child);\n        this.insets = insets;\n    }\n\n    public Padding(Insets insets) {\n        this(insets, null);\n    }\n\n    public Padding(Size size) {\n        this(Insets.right(size.width()).withTop(size.height()), null);\n    }\n\n\n    @Override\n    public OptionalChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends OptionalChildWidgetInstance<Padding> {\n\n        public Instance(Padding widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(Padding widget) {\n            if (Objects.equals(this.widget.insets, widget.insets)) return;\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var insets = this.widget.insets;\n            var childConstraints = Constraints.of(\n                Math.max(0, constraints.minWidth() - insets.horizontal()),\n                Math.max(0, constraints.minHeight() - insets.vertical()),\n                Math.max(0, constraints.maxWidth() - insets.horizontal()),\n                Math.max(0, constraints.maxHeight() - insets.vertical())\n            );\n\n            var size = (this.child != null ? this.child.layout(childConstraints) : Size.zero()).withInsets(insets).constrained(constraints);\n            this.transform.setSize(size);\n\n            if (this.child != null) {\n                this.child.transform.setX(insets.left());\n                this.child.transform.setY(insets.top());\n            }\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            var childWidth = this.child != null ? this.child.getIntrinsicWidth(height) : 0;\n            return childWidth + this.widget.insets.horizontal();\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            var childHeight = this.child != null ? this.child.getIntrinsicHeight(width) : 0;\n            return childHeight + this.widget.insets.vertical();\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            var childBaseline = this.child != null ? this.child.getBaselineOffset() : OptionalDouble.empty();\n            if (childBaseline.isEmpty()) return OptionalDouble.empty();\n\n            return OptionalDouble.of(childBaseline.getAsDouble() + this.widget.insets.top());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Panel.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.OptionalChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Panel extends OptionalChildInstanceWidget {\n\n    public static final Identifier VANILLA_LIGHT = Owo.id(\"panel/default\");\n    public static final Identifier VANILLA_DARK = Owo.id(\"panel/dark\");\n    public static final Identifier VANILLA_INSET = Owo.id(\"panel/inset\");\n\n    // ---\n\n    public final @Nullable Identifier texture;\n\n    public Panel(@Nullable Identifier texture, @Nullable Widget child) {\n        super(child);\n        this.texture = texture;\n    }\n\n    public Panel(Identifier texture) {\n        this(texture, null);\n    }\n\n    @Override\n    public OptionalChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends OptionalChildWidgetInstance.ShrinkWrap<Panel> {\n\n        public Instance(Panel widget) {\n            super(widget);\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            if (this.widget.texture != null) {\n                NinePatchTexture.draw(this.widget.texture, OwoUIGraphics.of(graphics), 0, 0, (int) this.transform.width(), (int) this.transform.height());\n            }\n\n            super.draw(graphics);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/RotatedLayout.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.CustomWidgetTransform;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetTransform;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.joml.Matrix3x2f;\n\nimport java.util.OptionalDouble;\n\npublic class RotatedLayout extends SingleChildInstanceWidget {\n\n    public final int increments;\n\n    public RotatedLayout(int increments, Widget child) {\n        super(child);\n        this.increments = increments;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<RotatedLayout> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance<RotatedLayout> {\n\n        public Instance(RotatedLayout widget) {\n            super(widget);\n            this.visualIncrements = Math.floorMod(widget.increments, 4);\n        }\n\n        @Override\n        protected WidgetTransform createTransform() {\n            var transform = new CustomWidgetTransform();\n            transform.setApplyAtCenter(false);\n\n            return transform;\n        }\n\n        private int visualIncrements;\n\n        private boolean isVertical() {\n            return this.visualIncrements % 2 == 1;\n        }\n\n        @Override\n        public void setWidget(RotatedLayout widget) {\n            if (this.visualIncrements == Math.floorMod(widget.increments, 4)) {\n                return;\n            }\n\n            super.setWidget(widget);\n\n            this.visualIncrements = Math.floorMod(widget.increments, 4);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var isVertical = this.isVertical();\n            var childConstraints = isVertical\n                ? Constraints.of(constraints.minHeight(), constraints.minWidth(), constraints.maxHeight(), constraints.maxWidth())\n                : constraints;\n\n            var childSize = this.child.layout(childConstraints);\n            var selfSize = isVertical\n                ? Size.of(childSize.height(), childSize.width())\n                : childSize;\n\n            this.transform.setSize(selfSize);\n\n            var childTransform = new Matrix3x2f()\n                .translate((float) (selfSize.width() / 2), (float) (selfSize.height() / 2))\n                .rotate((float) (this.visualIncrements * Math.PI / 2))\n                .translate((float) (-childSize.width() / 2), (float) (-childSize.height() / 2));\n\n            ((CustomWidgetTransform) this.transform).setMatrix(childTransform);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.isVertical() ? this.child.getIntrinsicHeight(height) : this.child.getIntrinsicWidth(height);\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.isVertical() ? this.child.getIntrinsicWidth(width) : this.child.getIntrinsicHeight(width);\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Sized.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Sized extends ConstraintWidget {\n\n    public final @Nullable Double width;\n    public final @Nullable Double height;\n\n    public Sized(@Nullable Double width, @Nullable Double height, Widget child) {\n        super(child);\n        this.width = width;\n        this.height = height;\n    }\n\n    public Sized(@Nullable Integer width, @Nullable Integer height, Widget child) {\n        this(width == null ? null : width.doubleValue(), height == null ? null : height.doubleValue(), child);\n    }\n\n    public Sized(double width, double height, Widget child) {\n        this((Double) width, (Double) height, child);\n    }\n\n    public Sized(Size size, Widget child) {\n        this(size.width(), size.height(), child);\n    }\n\n    @Override\n    protected Constraints constraints() {\n        return Constraints.tightOnAxis(this.width, this.height);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/TextureWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.framework.instance.InstanceHost;\nimport io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.OptionalChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.OptionalDouble;\n\npublic class TextureWidget extends OptionalChildInstanceWidget {\n\n    public final Identifier texture;\n    public final Wrap wrap;\n    public final Filter filter;\n    public final Color color;\n\n    public TextureWidget(Identifier texture, Wrap wrap, Filter filter, Color color, @Nullable Widget child) {\n        super(child);\n        this.texture = texture;\n        this.wrap = wrap;\n        this.filter = filter;\n        this.color = color;\n    }\n\n    public TextureWidget(Identifier texture, Wrap wrap, Color color, @Nullable Widget child) {\n        this(texture, wrap, Filter.TEXTURE_DEFAULT, color, child);\n    }\n\n    public TextureWidget(Identifier texture, Wrap wrap, Filter filter, Color color) {\n        this(texture, wrap, filter, color, null);\n    }\n\n    public TextureWidget(Identifier texture, Wrap wrap, Color color) {\n        this(texture, wrap, Filter.TEXTURE_DEFAULT, color);\n    }\n\n    @Override\n    public OptionalChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    // ---\n\n    public enum Wrap {\n        NONE, STRETCH, REPEAT\n    }\n\n    public enum Filter {\n        TEXTURE_DEFAULT, NEAREST, LINEAR;\n    }\n\n    // ---\n\n    public static class Instance extends OptionalChildWidgetInstance<TextureWidget> {\n\n        private @Nullable Size textureSize;\n\n        public Instance(TextureWidget widget) {\n            super(widget);\n        }\n\n        @Override\n        public void attachHost(InstanceHost host) {\n            super.attachHost(host);\n            this.refreshTextureSize();\n        }\n\n        @Override\n        public void setWidget(TextureWidget widget) {\n            super.setWidget(widget);\n            this.refreshTextureSize();\n        }\n\n        private void refreshTextureSize() {\n            var texture = this.host().client().getTextureManager().getTexture(widget.texture).getTexture();\n            var newTextureSize = Size.of(\n                texture.getWidth(0),\n                texture.getHeight(0)\n            );\n\n            if (!newTextureSize.equals(this.textureSize)) {\n                this.markNeedsLayout();\n            }\n\n            this.textureSize = newTextureSize;\n        }\n\n        private double imageAspectRatio() {\n            //noinspection DataFlowIssue\n            return this.textureSize.width() / this.textureSize.height();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            if (this.child == null) {\n                var size = this.textureSize != null\n                    ? AspectRatio.applyAspectRatio(constraints, this.textureSize)\n                    : constraints.maxFiniteOrMinSize();\n\n                this.transform.setSize(size);\n            } else {\n                this.sizeToChild(constraints, this.child);\n            }\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.child != null\n                ? this.child.getIntrinsicWidth(height)\n                : this.textureSize != null\n                    ? Double.isFinite(height) ? height * this.imageAspectRatio() : this.textureSize.width()\n                    : 0;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.child != null\n                ? this.child.getIntrinsicHeight(width)\n                : this.textureSize != null\n                    ? Double.isFinite(width) ? width / this.imageAspectRatio() : this.textureSize.height()\n                    : 0;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.child != null ? this.child.getBaselineOffset() : OptionalDouble.empty();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            var matrices = graphics.pose();\n            var stretch = this.widget.wrap == Wrap.STRETCH;\n\n            var textureWidth = (int) (this.textureSize != null ? this.textureSize.width() : this.transform.width());\n            var textureHeight = (int) (this.textureSize != null ? this.textureSize.height() : this.transform.height());\n\n            var quadWidth = (int) (this.widget.wrap != Wrap.REPEAT ? textureWidth : this.transform.width());\n            var quadHeight = (int) (this.widget.wrap != Wrap.REPEAT ? textureHeight : this.transform.height());\n\n            if (stretch) {\n                matrices.pushMatrix();\n                matrices.scale((int) this.transform.width() / (float) textureWidth, (int) this.transform.height() / (float) textureHeight);\n            }\n\n            var pipeline = switch (this.widget.filter) {\n                case TEXTURE_DEFAULT -> BraidRenderPipelines.TEXTURED_DEFAULT;\n                case NEAREST -> BraidRenderPipelines.TEXTURED_NEAREST;\n                case LINEAR -> BraidRenderPipelines.TEXTURED_BILINEAR;\n            };\n\n            graphics.blit(\n                pipeline,\n                this.widget.texture,\n                0, 0, 0, 0,\n                quadWidth, quadHeight,\n                textureWidth, textureHeight,\n                this.widget.color.argb()\n            );\n\n            if (stretch) {\n                matrices.popMatrix();\n            }\n\n            super.draw(graphics);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Tooltip.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.framework.instance.InstanceHost;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.TooltipProvider;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTextTooltip;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Collection;\nimport java.util.List;\n\npublic class Tooltip extends SingleChildInstanceWidget {\n\n    public final @Nullable List<ClientTooltipComponent> tooltip;\n    public final Component tooltipText;\n\n    public Tooltip(@NotNull List<ClientTooltipComponent> tooltip, Widget child) {\n        super(child);\n        this.tooltip = tooltip;\n        this.tooltipText = null;\n    }\n\n    public Tooltip(Collection<Component> tooltip, Widget child) {\n        this(\n            tooltip.stream().map(Component::getVisualOrderText).<ClientTooltipComponent>map(ClientTextTooltip::new).toList(),\n            child\n        );\n    }\n\n    public Tooltip(Component tooltip, Widget child) {\n        super(child);\n        this.tooltip = null;\n        this.tooltipText = tooltip;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<Tooltip> implements TooltipProvider {\n        private @Nullable List<ClientTooltipComponent> tooltip;\n\n        public Instance(Tooltip widget) {\n            super(widget);\n        }\n\n        @Override\n        public void attachHost(InstanceHost host) {\n            super.attachHost(host);\n            this.setup();\n        }\n\n        @Override\n        public void setWidget(Tooltip widget) {\n            super.setWidget(widget);\n            this.setup();\n        }\n\n        private void setup() {\n            this.tooltip = widget.tooltipText != null\n                ? this.host().client().font\n                .split(widget.tooltipText, Integer.MAX_VALUE)\n                .stream()\n                .<ClientTooltipComponent>map(ClientTextTooltip::new)\n                .toList()\n                : widget.tooltip;\n        }\n\n        @Override\n        public @Nullable List<ClientTooltipComponent> getTooltipComponentsAt(double x, double y) {\n            return tooltip;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Transform.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.framework.instance.CustomWidgetTransform;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetTransform;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.joml.Matrix3x2f;\n\nimport java.util.Objects;\n\npublic class Transform extends SingleChildInstanceWidget {\n\n    public final Matrix3x2f matrix;\n\n    public Transform(Matrix3x2f matrix, Widget child) {\n        super(child);\n        this.matrix = matrix;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<Transform> {\n\n        public Instance(Transform widget) {\n            super(widget);\n            ((CustomWidgetTransform) this.transform).setMatrix(this.widget.matrix);\n        }\n\n        @Override\n        public void setWidget(Transform widget) {\n            if (Objects.equals(this.widget.matrix, widget.matrix)) {\n                this.transform.recompute();\n                return;\n            }\n\n            super.setWidget(widget);\n            ((CustomWidgetTransform) this.transform).setMatrix(this.widget.matrix);\n\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected WidgetTransform createTransform() {\n            return new CustomWidgetTransform();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/Visibility.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.HitTestState;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.OptionalDouble;\n\npublic class Visibility extends SingleChildInstanceWidget {\n\n    public final boolean visible;\n    public final boolean reportSize;\n\n    public Visibility(boolean visible, boolean reportSize, Widget child) {\n        super(child);\n        this.visible = visible;\n        this.reportSize = reportSize;\n    }\n\n    public Visibility(boolean visible, Widget child) {\n        this(visible, false, child);\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance<Visibility> {\n\n        public Instance(Visibility widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(Visibility widget) {\n            if (this.widget.visible == widget.visible\n                && this.widget.reportSize == widget.reportSize) {\n                return;\n            }\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var childSize = this.child.layout(constraints);\n            if (this.widget.visible || this.widget.reportSize) {\n                this.transform.setSize(childSize);\n            } else {\n                this.transform.setSize(Size.zero());\n            }\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.widget.visible || this.widget.reportSize ? this.child.getIntrinsicWidth(height) : 0;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.widget.visible || this.widget.reportSize ? this.child.getIntrinsicHeight(width) : 0;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.widget.visible || this.widget.reportSize ? this.child.getBaselineOffset() : OptionalDouble.empty();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            if (!this.widget.visible) return;\n            super.draw(graphics);\n        }\n\n        @Override\n        public void hitTest(double x, double y, HitTestState state) {\n            if (!this.widget.visible) return;\n            super.hitTest(x, y, state);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/basic/VisitorWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.basic;\n\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.ComposedProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic abstract class VisitorWidget extends Widget {\n    public final Widget child;\n\n    protected VisitorWidget(Widget child) {\n        this.child = child;\n    }\n\n    @Override\n    public abstract Proxy<?> proxy();\n\n    public static class Proxy<T extends VisitorWidget> extends ComposedProxy {\n\n        public final VisitorWidget.Visitor<T> visitor;\n        public WidgetInstance<?> descendantInstance;\n\n        public Proxy(Widget widget, VisitorWidget.Visitor<T> visitor) {\n            super(widget);\n            this.visitor = visitor;\n        }\n\n        @Override\n        public void mount(WidgetProxy parent, @Nullable Object slot) {\n            super.mount(parent, slot);\n            this.rebuild();\n        }\n\n        @Override\n        public void updateWidget(Widget newWidget) {\n            super.updateWidget(newWidget);\n            this.rebuild(true);\n        }\n\n        @Override\n        protected void doRebuild() {\n            super.doRebuild();\n            this.child = this.refreshChild(this.child, ((VisitorWidget)this.widget()).child, this.slot());\n\n            if (this.descendantInstance != null) {\n                this.visitor.visit((T) this.widget(), this.descendantInstance);\n            }\n        }\n\n        @Override\n        public void notifyDescendantInstance(@Nullable WidgetInstance<?> instance, @Nullable Object slot) {\n            this.visitor.visit((T) this.widget(), instance);\n            this.descendantInstance = instance;\n        }\n    }\n\n    @FunctionalInterface\n    public interface Visitor<T> {\n        void visit(T widget, WidgetInstance<?> instance);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/button/Button.java",
    "content": "package io.wispforest.owo.braid.widgets.button;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.ControlsOverride;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.BooleanSupplier;\n\npublic class Button extends StatelessWidget {\n\n    public final @Nullable ButtonStyle style;\n    public final @Nullable BooleanSupplier onClick;\n    public final Widget child;\n\n    public Button(@Nullable ButtonStyle style, @Nullable BooleanSupplier onClick, Widget child) {\n        this.onClick = onClick;\n        this.style = style;\n        this.child = child;\n    }\n\n    public Button(@Nullable ButtonStyle style, @Nullable Runnable onClick, Widget child) {\n        this(style, Clickable.alwaysClick(onClick), child);\n    }\n\n    public Button(@Nullable BooleanSupplier onClick, Widget child) {\n        this(null, onClick, child);\n    }\n\n    public Button(@Nullable Runnable onClick, Widget child) {\n        this(Clickable.alwaysClick(onClick), child);\n    }\n\n    public Button(@Nullable ButtonStyle style, boolean active, BooleanSupplier onClick, Widget child) {\n        this(style, active ? onClick : null, child);\n    }\n\n    public Button(@Nullable ButtonStyle style, boolean active, Runnable onClick, Widget child) {\n        this(style, active, Clickable.alwaysClick(onClick), child);\n    }\n\n    public Button(boolean active, BooleanSupplier onClick, Widget child) {\n        this(null, active, onClick, child);\n    }\n\n    public Button(boolean active, Runnable onClick, Widget child) {\n        this(active, Clickable.alwaysClick(onClick), child);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        var effectiveStyle = this.style != null ? this.style : ButtonStyle.DEFAULT;\n        if (DefaultButtonStyle.maybeOf(context) instanceof ButtonStyle contextStyle) {\n            effectiveStyle = effectiveStyle.overriding(contextStyle);\n        }\n\n        Widget content = new Padding(\n            effectiveStyle.padding() != null\n                ? effectiveStyle.padding()\n                : Insets.all(5),\n            this.child\n        );\n\n        var disabled = this.onClick == null || ControlsOverride.controlsDisabled(context);\n        content = effectiveStyle.builder() != null\n            ? effectiveStyle.builder().build(!disabled, content)\n            : new ButtonPanel(!disabled, content);\n\n        return new Clickable(\n            this.onClick,\n            effectiveStyle.clickSound(),\n            content\n        );\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/button/ButtonPanel.java",
    "content": "package io.wispforest.owo.braid.widgets.button;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.HoverableBuilder;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport io.wispforest.owo.ui.component.ButtonComponent;\n\npublic class ButtonPanel extends StatelessWidget {\n    public final boolean active;\n    public final Widget child;\n\n    public ButtonPanel(boolean active, Widget child) {\n        this.active = active;\n        this.child = child;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new HoverableBuilder(\n            (innerContext, hovered, child) -> {\n                return new Panel(\n                    this.active\n                        ? (hovered || Focusable.shouldShowHighlight(context))\n                        ? ButtonComponent.HOVERED_TEXTURE\n                        : ButtonComponent.ACTIVE_TEXTURE\n                        : ButtonComponent.DISABLED_TEXTURE,\n                    child\n                );\n            },\n            this.child\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/button/ButtonStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.button;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.sounds.SoundEvent;\nimport org.jetbrains.annotations.Nullable;\n\npublic record ButtonStyle(\n    @Nullable ContentBuilder builder,\n    @Nullable Insets padding,\n    @Nullable SoundEvent clickSound\n) {\n    public ButtonStyle overriding(ButtonStyle other) {\n        return new ButtonStyle(\n            this.builder != null ? this.builder : other.builder,\n            this.padding != null ? this.padding : other.padding,\n            this.clickSound != null ? this.clickSound : other.clickSound\n        );\n    }\n\n    public static final ButtonStyle DEFAULT = new ButtonStyle(null, null, null);\n\n    @FunctionalInterface\n    public interface ContentBuilder {\n        Widget build(boolean active, Widget child);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/button/Clickable.java",
    "content": "package io.wispforest.owo.braid.widgets.button;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.ControlsOverride;\nimport io.wispforest.owo.braid.widgets.intents.Interactable;\nimport io.wispforest.owo.ui.util.UISounds;\nimport net.minecraft.sounds.SoundEvent;\nimport net.minecraft.sounds.SoundEvents;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.BooleanSupplier;\n\npublic class Clickable extends StatelessWidget {\n\n    public final @Nullable BooleanSupplier onClick;\n    public final @Nullable SoundEvent clickSound;\n    public final Widget child;\n\n    public Clickable(@Nullable BooleanSupplier onClick, @Nullable SoundEvent clickSound, Widget child) {\n        this.onClick = onClick;\n        this.clickSound = clickSound;\n        this.child = child;\n    }\n\n    public Clickable(@Nullable BooleanSupplier onClick, Widget child) {\n        this(onClick, null, child);\n    }\n\n    public Clickable(boolean active, BooleanSupplier onClick, @Nullable SoundEvent clickSound, Widget child) {\n        this(active ? onClick : null, clickSound, child);\n    }\n\n    public Clickable(boolean active, BooleanSupplier onClick, Widget child) {\n        this(active, onClick, null, child);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        if (this.onClick == null || ControlsOverride.controlsDisabled(context)) {\n            return this.child;\n        }\n\n        var effectiveSound = this.clickSound != null ? this.clickSound : SoundEvents.UI_BUTTON_CLICK.value();\n        return Interactable.primary(\n            () -> {\n                if (this.onClick.getAsBoolean()) {\n                    UISounds.play(effectiveSound);\n                }\n            },\n            this.child\n        );\n    }\n\n    // ---\n\n    public static @Nullable BooleanSupplier alwaysClick(@Nullable Runnable onClick) {\n        if (onClick == null) {\n            return null;\n        }\n\n        return () -> {\n            onClick.run();\n            return true;\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/button/DefaultButtonStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.button;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport org.jetbrains.annotations.Nullable;\n\npublic class DefaultButtonStyle extends InheritedWidget {\n\n    public final ButtonStyle style;\n\n    public DefaultButtonStyle(ButtonStyle style, Widget child) {\n        super(child);\n        this.style = style;\n    }\n\n    public static Widget merge(ButtonStyle style, Widget child) {\n        return new Builder(context -> {\n            var contextStyle = DefaultButtonStyle.maybeOf(context);\n            return new DefaultButtonStyle(contextStyle != null ? style.overriding(contextStyle) : style, child);\n        });\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return !this.style.equals(((DefaultButtonStyle) newWidget).style);\n    }\n\n    public static @Nullable ButtonStyle maybeOf(BuildContext context) {\n        var widget = context.dependOnAncestor(DefaultButtonStyle.class);\n        if (widget != null) {\n            return widget.style;\n        } else {\n            return null;\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/button/MessageButton.java",
    "content": "package io.wispforest.owo.braid.widgets.button;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\npublic class MessageButton extends StatelessWidget {\n\n    public final Component text;\n    public final @Nullable Runnable onClick;\n\n    public MessageButton(Component text, @Nullable Runnable onClick) {\n        this.text = text;\n        this.onClick = onClick;\n    }\n\n    public MessageButton(Component text, boolean active, Runnable onClick) {\n        this(text, active ? onClick : null);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Button(\n            this.onClick,\n            //TODO: abstract away the million places where a ternary operator is used to determine the label style for a possibly disabled button\n            new Label(\n                this.onClick != null\n                    ? LabelStyle.SHADOW\n                    : new LabelStyle(null, Color.formatting(ChatFormatting.GRAY), null, false),\n                true,\n                this.text\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/checkbox/Checkbox.java",
    "content": "package io.wispforest.owo.braid.widgets.checkbox;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.basic.ControlsOverride;\nimport io.wispforest.owo.braid.widgets.checkbox.TogglingClickable.CheckboxCallback;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport net.minecraft.client.resources.model.Material;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Checkbox extends StatelessWidget {\n\n    public final @Nullable CheckboxStyle style;\n    public final boolean checked;\n    public final @Nullable CheckboxCallback onUpdate;\n\n    public Checkbox(@Nullable CheckboxStyle style, boolean checked, @Nullable CheckboxCallback onUpdate) {\n        this.checked = checked;\n        this.style = style;\n        this.onUpdate = onUpdate;\n    }\n\n    public Checkbox(boolean checked, @Nullable CheckboxCallback onUpdate) {\n        this(null, checked, onUpdate);\n    }\n\n    public Checkbox(@Nullable CheckboxStyle style, boolean checked, boolean active, CheckboxCallback onUpdate) {\n        this(style, checked, active ? onUpdate : null);\n    }\n\n    public Checkbox(boolean checked, boolean active, CheckboxCallback onUpdate) {\n        this(null, checked, active, onUpdate);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        var effectiveStyle = this.style != null ? this.style : CheckboxStyle.DEFAULT;\n        if (DefaultCheckboxStyle.maybeOf(context) instanceof CheckboxStyle contextStyle) {\n            effectiveStyle = effectiveStyle.overriding(contextStyle);\n        }\n\n        var disabled = this.onUpdate == null || ControlsOverride.controlsDisabled(context);\n        var background = effectiveStyle.backgroundBuilder() != null\n            ? effectiveStyle.backgroundBuilder().build(disabled)\n            : DEFAULT_BACKGROUND;\n\n        var checkmark = effectiveStyle.checkmark() != null\n            ? effectiveStyle.checkmark()\n            : DEFAULT_CHECKMARK;\n\n        return new TogglingClickable(\n            this.checked,\n            this.onUpdate,\n            effectiveStyle.clickSound(),\n            this.checked\n                ? new Stack(new StackBase(background), checkmark)\n                : background\n        );\n    }\n\n    // ---\n\n    public static final Material SELECTED_HIGHLIGHTED_TEXTURE = new Material(\n        SpriteWidget.GUI_ATLAS_ID,\n        Identifier.withDefaultNamespace(\"widget/checkbox_selected_highlighted\")\n    );\n\n    public static final Material SELECTED_TEXTURE = new Material(\n        SpriteWidget.GUI_ATLAS_ID,\n        Identifier.withDefaultNamespace(\"widget/checkbox_selected\")\n    );\n\n    public static final Material HIGHLIGHTED_TEXTURE = new Material(\n        SpriteWidget.GUI_ATLAS_ID,\n        Identifier.withDefaultNamespace(\"widget/checkbox_highlighted\")\n    );\n\n    public static final Material TEXTURE = new Material(\n        SpriteWidget.GUI_ATLAS_ID,\n        Identifier.withDefaultNamespace(\"widget/checkbox\")\n    );\n\n    // ---\n\n    private static final Widget DEFAULT_BACKGROUND = new Builder(context -> {\n        return new SpriteWidget(Focusable.shouldShowHighlight(context) ? HIGHLIGHTED_TEXTURE : TEXTURE);\n    });\n\n    private static final Widget DEFAULT_CHECKMARK = new Builder(context -> {\n        return new SpriteWidget(Focusable.shouldShowHighlight(context) ? SELECTED_HIGHLIGHTED_TEXTURE : SELECTED_TEXTURE);\n    });\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/checkbox/CheckboxStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.checkbox;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport net.minecraft.client.resources.model.Material;\nimport net.minecraft.sounds.SoundEvent;\nimport org.jetbrains.annotations.Nullable;\n\npublic record CheckboxStyle(\n    @Nullable BackgroundBuilder backgroundBuilder,\n    @Nullable Widget checkmark,\n    @Nullable SoundEvent clickSound\n) {\n    public CheckboxStyle overriding(CheckboxStyle other) {\n        return new CheckboxStyle(\n            this.backgroundBuilder != null ? this.backgroundBuilder : other.backgroundBuilder,\n            this.checkmark != null ? this.checkmark : other.checkmark,\n            this.clickSound != null ? this.clickSound : other.clickSound\n        );\n    }\n\n    public static final CheckboxStyle DEFAULT = new CheckboxStyle(null, null, null);\n\n    @FunctionalInterface\n    public interface BackgroundBuilder {\n        Widget build(boolean active);\n    }\n\n    // ---\n\n    public static final Material BRAID_BACKGROUND_TEXTURE = new Material(\n        SpriteWidget.GUI_ATLAS_ID,\n        Owo.id(\"braid_checkbox\")\n    );\n\n    public static final Material BRAID_BACKGROUND_FOCUSED_TEXTURE = new Material(\n        SpriteWidget.GUI_ATLAS_ID,\n        Owo.id(\"braid_checkbox_focused\")\n    );\n\n    public static final Material BRAID_CHECKMARK_TEXTURE = new Material(\n        SpriteWidget.GUI_ATLAS_ID,\n        Owo.id(\"braid_checkmark\")\n    );\n\n    private static final Widget BRAID_BACKGROUND = new Builder(context -> {\n        return new SpriteWidget(Focusable.shouldShowHighlight(context) ? BRAID_BACKGROUND_FOCUSED_TEXTURE : BRAID_BACKGROUND_TEXTURE);\n    });\n\n    private static final Widget BRAID_CHECKMARK = new Center(new SpriteWidget(BRAID_CHECKMARK_TEXTURE));\n\n    public static final CheckboxStyle BRAID = new CheckboxStyle(\n        active -> BRAID_BACKGROUND,\n        BRAID_CHECKMARK,\n        null\n    );\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/checkbox/DefaultCheckboxStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.checkbox;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport org.jetbrains.annotations.Nullable;\n\npublic class DefaultCheckboxStyle extends InheritedWidget {\n\n    public final CheckboxStyle style;\n\n    public DefaultCheckboxStyle(CheckboxStyle style, Widget child) {\n        super(child);\n        this.style = style;\n    }\n\n    public static Widget merge(CheckboxStyle style, Widget child) {\n        return new Builder(context -> {\n            var contextStyle = DefaultCheckboxStyle.maybeOf(context);\n            return new DefaultCheckboxStyle(contextStyle != null ? style.overriding(contextStyle) : style, child);\n        });\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return !this.style.equals(((DefaultCheckboxStyle) newWidget).style);\n    }\n\n    public static @Nullable CheckboxStyle maybeOf(BuildContext context) {\n        var widget = context.dependOnAncestor(DefaultCheckboxStyle.class);\n        if (widget != null) {\n            return widget.style;\n        } else {\n            return null;\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/checkbox/TogglingClickable.java",
    "content": "package io.wispforest.owo.braid.widgets.checkbox;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.button.Clickable;\nimport net.minecraft.sounds.SoundEvent;\nimport org.jetbrains.annotations.Nullable;\n\npublic class TogglingClickable extends StatelessWidget {\n\n    public final boolean checked;\n    public final @Nullable CheckboxCallback onUpdate;\n    public final @Nullable SoundEvent clickSound;\n    public final Widget child;\n\n    public TogglingClickable(boolean checked, @Nullable CheckboxCallback onUpdate, @Nullable SoundEvent clickSound, Widget child) {\n        this.checked = checked;\n        this.onUpdate = onUpdate;\n        this.clickSound = clickSound;\n        this.child = child;\n    }\n\n    public TogglingClickable(boolean checked, @Nullable CheckboxCallback onUpdate, Widget child) {\n        this(checked, onUpdate, null, child);\n    }\n\n    public TogglingClickable(boolean checked, boolean active, @Nullable SoundEvent clickSound, CheckboxCallback onUpdate, Widget child) {\n        this(checked, active ? onUpdate : null, clickSound, child);\n    }\n\n    public TogglingClickable(boolean checked, boolean active, CheckboxCallback onUpdate, Widget child) {\n        this(checked, active, null, onUpdate, child);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Clickable(\n            this.onUpdate != null ? Clickable.alwaysClick(() -> this.onUpdate.accept(!this.checked)) : null,\n            this.clickSound,\n            this.child\n        );\n    }\n\n    @FunctionalInterface\n    public interface CheckboxCallback {\n        void accept(boolean nowChecked);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/collapsible/Collapsible.java",
    "content": "package io.wispforest.owo.braid.widgets.collapsible;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.intents.Interactable;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\n\nimport java.util.ArrayList;\n\npublic class Collapsible extends StatefulWidget {\n\n    public final boolean showVerticalRule;\n\n    public final boolean collapsed;\n    public final CollapsibleCallback onToggled;\n\n    public final Widget title;\n    public final Widget content;\n\n    public Collapsible(boolean showVerticalRule, boolean collapsed, CollapsibleCallback onToggled, Widget title, Widget content) {\n        this.showVerticalRule = showVerticalRule;\n        this.collapsed = collapsed;\n        this.onToggled = onToggled;\n        this.title = title;\n        this.content = content;\n    }\n\n    @Override\n    public WidgetState<Collapsible> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<Collapsible> {\n\n        public boolean hovered = false;\n\n        @Override\n        public Widget build(BuildContext context) {\n            var body = new ArrayList<Widget>();\n\n            if (this.widget().showVerticalRule) {\n                body.add(new Align(\n                    Alignment.LEFT,\n                    new Padding(\n                        Insets.left(6),\n                        new Sized(\n                            1,\n                            Double.POSITIVE_INFINITY,\n                            new Box(\n                                this.hovered ? Color.WHITE : Color.mix(.5f, Color.WHITE, Color.BLACK)\n                            )\n                        )\n                    )\n                ));\n            }\n\n            body.add(new StackBase(\n                new Padding(Insets.left(10), this.widget().content)\n            ));\n\n            return new Column(\n                new MouseArea(\n                    widget -> widget\n                        .enterCallback(this.widget().showVerticalRule ? () -> this.setState(() -> this.hovered = true) : null)\n                        .exitCallback(this.widget().showVerticalRule ? () -> this.setState(() -> this.hovered = false) : null),\n                    new Row(\n                        MainAxisAlignment.START,\n                        CrossAxisAlignment.CENTER,\n                        new Sized(\n                            12,\n                            12,\n                            Interactable.primary(\n                                () -> this.widget().onToggled.onToggled(!this.widget().collapsed),\n                                new Center(\n                                    new SpriteWidget(Owo.id(this.widget().collapsed ? \"braid_collapsible_closed\" : \"braid_collapsible_open\"))\n                                )\n                            )\n                        ),\n                        this.widget().title\n                    )\n                ),\n                new Visibility(\n                    !this.widget().collapsed,\n                    new Stack(\n                        body\n                    )\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/collapsible/CollapsibleCallback.java",
    "content": "package io.wispforest.owo.braid.widgets.collapsible;\n\n@FunctionalInterface\npublic interface CollapsibleCallback {\n    void onToggled(boolean nowCollapsed);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/collapsible/LazyCollapsible.java",
    "content": "package io.wispforest.owo.braid.widgets.collapsible;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\n\npublic class LazyCollapsible extends StatefulWidget {\n    public final boolean showVerticalRule;\n\n    public final boolean collapsed;\n    public final CollapsibleCallback onToggled;\n\n    public final Widget title;\n    public final Widget content;\n\n    public LazyCollapsible(boolean showVerticalRule, boolean collapsed, CollapsibleCallback onToggled, Widget title, Widget content) {\n        this.showVerticalRule = showVerticalRule;\n        this.collapsed = collapsed;\n        this.onToggled = onToggled;\n        this.title = title;\n        this.content = content;\n    }\n\n    @Override\n    public WidgetState<LazyCollapsible> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<LazyCollapsible> {\n\n        public boolean expandedOnce = false;\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            if (!widget.collapsed && !this.expandedOnce) {\n                this.expandedOnce = true;\n            }\n\n            return new Collapsible(\n                widget.showVerticalRule,\n                widget.collapsed,\n                widget.onToggled,\n                widget.title,\n                this.expandedOnce ? widget.content : new Padding(Insets.none())\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/combobox/ComboBox.java",
    "content": "package io.wispforest.owo.braid.widgets.combobox;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.ListenableValue;\nimport io.wispforest.owo.braid.core.RelativePosition;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.basic.HoverableBuilder;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.intents.*;\nimport io.wispforest.owo.braid.widgets.overlay.Overlay;\nimport io.wispforest.owo.braid.widgets.overlay.OverlayEntry;\nimport io.wispforest.owo.braid.widgets.overlay.OverlayEntryBuilder;\nimport io.wispforest.owo.braid.widgets.textinput.EditableText;\nimport io.wispforest.owo.braid.widgets.textinput.TextEditingController;\nimport io.wispforest.owo.braid.widgets.textinput.TextEditingValue;\nimport io.wispforest.owo.braid.widgets.textinput.TextSelection;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.OptionalInt;\nimport java.util.function.Function;\n\npublic class ComboBox<T> extends StatefulWidget {\n\n    public static final Identifier ACTIVE_TEXTURE = Owo.id(\"braid_combobox/active\");\n    public static final Identifier HOVERED_TEXTURE = Owo.id(\"braid_combobox/hovered\");\n    public static final Identifier DISABLED_TEXTURE = Owo.id(\"braid_combobox/disabled\");\n\n    // ---\n\n    public final Function<T, Component> optionToName;\n\n    public final List<T> options;\n    public final @Nullable T selectedOption;\n    public final SelectCallback<T> onSelect;\n\n    public ComboBox(Function<T, Component> optionToName, List<T> options, @Nullable T selectedOption, SelectCallback<T> onSelect) {\n        this.optionToName = optionToName;\n        this.options = options;\n        this.selectedOption = selectedOption;\n        this.onSelect = onSelect;\n    }\n\n    public ComboBox(List<T> options, @Nullable T selectedOption, SelectCallback<T> onSelect) {\n        this(option -> Component.literal(Objects.toString(option)), options, selectedOption, onSelect);\n    }\n\n    @Override\n    public WidgetState<ComboBox<T>> createState() {\n        return new State<>();\n    }\n\n    public List<Component> optionNames() {\n        return this.options.stream().map(this::nameOption).toList();\n    }\n\n    public Component nameOption(@Nullable T option) {\n        return option != null\n            ? this.optionToName.apply(option)\n            : Component.empty();\n    }\n\n    public interface SelectCallback<T> {\n        void onSelect(T option);\n    }\n\n    private static class State<T> extends WidgetState<ComboBox<T>> {\n\n        private final Runnable listener = this::textListener;\n\n        private TextEditingController controller;\n        private String lastText;\n\n        private @Nullable OverlayEntry currentOverlay;\n        private @Nullable ListenableValue<ComboBoxButtonsState<T>> buttonsState;\n\n        private boolean isOpen() {\n            return this.currentOverlay != null;\n        }\n\n        @Override\n        public void init() {\n            this.controller = new TextEditingController(this.widget().nameOption(widget().selectedOption).getString());\n            this.controller.addListener(this.listener);\n\n        }\n\n        @Override\n        public void didUpdateWidget(ComboBox<T> oldWidget) {\n            if (!Objects.equals(this.widget().selectedOption, oldWidget.selectedOption)) {\n                this.resetTextInput();\n            }\n        }\n\n        @Override\n        public void dispose() {\n            this.controller.removeListener(this.listener);\n            if (this.currentOverlay != null) {\n                this.currentOverlay.remove();\n            }\n        }\n\n        private void textListener() {\n            if (Objects.equals(this.controller.value().text(), this.lastText)) return;\n            this.lastText = controller.value().text();\n\n            if (this.widget().optionNames().stream().map(Component::getString).anyMatch(s -> s.equals(this.controller.value().text()))) {\n                return;\n            }\n\n            if (!this.isOpen()) {\n                this.open();\n            }\n\n            this.buttonsState.setValue(new ComboBoxButtonsState<>(\n                this.widget().options.stream()\n                    .filter(option -> this.widget().nameOption(option).getString().startsWith(this.controller.value().text()))\n                    .toList(),\n                OptionalInt.empty()\n            ));\n        }\n\n        private void resetTextInput() {\n            var text = this.widget().nameOption(this.widget().selectedOption).getString();\n            this.controller.setValue(new TextEditingValue(\n                text,\n                TextSelection.collapsed(text.length())\n            ));\n        }\n\n        private void select(T option) {\n            this.widget().onSelect.onSelect(option);\n            this.resetTextInput();\n\n            if (this.currentOverlay != null) {\n                this.currentOverlay.remove();\n            }\n        }\n\n        private void trySelectHighlightedValue() {\n            if (this.buttonsState == null) return;\n\n            var state = this.buttonsState.value();\n            if (state.highlightedOptionIdx().isEmpty() && state.options().isEmpty()) {\n                return;\n            }\n\n            this.select(\n                state.highlightedOptionIdx().isPresent()\n                    ? state.options().get(state.highlightedOptionIdx().getAsInt())\n                    : state.options().getFirst()\n            );\n        }\n\n        private void cycle(int offset) {\n            if (this.isOpen()) {\n                var state = this.buttonsState.value();\n\n                var currentOptionIdx = state.highlightedOptionIdx().orElse(offset > 0 ? -1 : 0);\n                var nextOptionIdx = Math.floorMod(currentOptionIdx + offset, state.options().size());\n\n                this.buttonsState.setValue(new ComboBoxButtonsState<>(\n                    state.options(),\n                    OptionalInt.of(nextOptionIdx)\n                ));\n            } else {\n                var currentOptionIdx = this.widget().selectedOption != null\n                    ? this.widget().options.indexOf(this.widget().selectedOption)\n                    : -Integer.signum(offset);\n                var nextOptionIdx = Math.floorMod(currentOptionIdx + offset, this.widget().options.size());\n\n                this.select(this.widget().options.get(nextOptionIdx));\n            }\n        }\n\n        private void open() {\n            this.setState(() -> {\n                this.buttonsState = new ListenableValue<>(new ComboBoxButtonsState<>(this.widget().options, OptionalInt.empty()));\n                this.currentOverlay = Overlay.of(this.context()).add(\n                    new OverlayEntryBuilder(\n                        new ComboBoxButtons<>(\n                            this.buttonsState,\n                            this.context().instance().transform.width(),\n                            this.widget()::nameOption,\n                            this::select\n                        ),\n                        new RelativePosition(this.context(), 0, this.context().instance().transform.height() - 1)\n                    )\n                        .dismissOverlayOnClick()\n                        .onRemove(() -> this.setState(() -> {\n                            this.currentOverlay = null;\n                            this.buttonsState = null;\n                        }))\n                );\n            });\n        }\n\n        private void close() {\n            if (this.currentOverlay != null) {\n                this.currentOverlay.remove();\n            }\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var expanded = this.isOpen();\n\n            return new Interactable(\n                SHORTCUTS,\n                widget -> widget\n                    .focusLostCallback(this::resetTextInput)\n                    .cursorStyle(CursorStyle.HAND)\n                    .addCallbackAction(CycleIntent.class, (actionCtx, intent) -> this.cycle(intent.previous() ? -1 : 1))\n                    .addCallbackAction(SelectIntent.class, (actionCtx, intent) -> {\n                        if (expanded) {\n                            this.trySelectHighlightedValue();\n                        } else {\n                            this.open();\n                        }\n                    })\n                    .addCallbackAction(PrimaryActionIntent.class, (actionCtx, intent) -> {\n                        if (expanded) {\n                            this.close();\n                        } else {\n                            this.open();\n                        }\n                    }),\n                new HoverableBuilder(\n                    (hoverableContext, hovered, child) -> new Panel(\n                        (expanded || hovered) ? HOVERED_TEXTURE : ACTIVE_TEXTURE,\n                        child\n                    ),\n                    new Padding(\n                        Insets.of(4, 4, 6, 0),\n                        new Row(\n                            MainAxisAlignment.START,\n                            CrossAxisAlignment.CENTER,\n                            new Flexible(\n                                new EditableText(\n                                    this.controller,\n                                    widget -> widget\n                                        .textShadow(true)\n                                        .singleLine()\n                                )\n                            ),\n                            new Padding(\n                                Insets.horizontal(3),\n                                new SpriteWidget(Owo.id(\"braid_combo_box_arrow\"))\n                            )\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    // ---\n\n    private static final Map<List<ShortcutTrigger>, Intent> SHORTCUTS = Map.of(\n        List.of(ShortcutTrigger.UP), new CycleIntent(true),\n        List.of(ShortcutTrigger.DOWN), new CycleIntent(false),\n        List.of(new ShortcutTrigger(Trigger.ofKey(GLFW.GLFW_KEY_ENTER), Trigger.ofKey(GLFW.GLFW_KEY_KP_ENTER))), new SelectIntent(),\n        List.of(ShortcutTrigger.LEFT_CLICK), PrimaryActionIntent.INSTANCE\n    );\n}\n\nrecord CycleIntent(boolean previous) implements Intent {}\nrecord SelectIntent() implements Intent {}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/combobox/ComboBoxButtons.java",
    "content": "package io.wispforest.owo.braid.widgets.combobox;\n\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.Clickable;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.scroll.FlatScrollbar;\nimport io.wispforest.owo.braid.widgets.scroll.Scrollable;\nimport io.wispforest.owo.braid.widgets.scroll.ScrollableWithBars;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.function.Function;\n\nclass ComboBoxButtons<T> extends StatelessWidget {\n\n    public final ListenableValue<ComboBoxButtonsState<T>> state;\n    public final double width;\n    public final Function<@Nullable T, Component> optionToName;\n    public final ComboBox.SelectCallback<T> onSelect;\n\n    public ComboBoxButtons(ListenableValue<ComboBoxButtonsState<T>> state, double width, Function<@Nullable T, Component> optionToName, ComboBox.SelectCallback<T> onSelect) {\n        this.state = state;\n        this.width = width;\n        this.optionToName = optionToName;\n        this.onSelect = onSelect;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Sized(\n            this.width,\n            null,\n            new Box(\n                Color.WHITE,\n                true,\n                new Padding(\n                    Insets.all(1),\n                    new Blur(\n                        5,\n                        10,\n                        false,\n                        new Box(\n                            Color.BLACK.withA(.6),\n                            new ListenableBuilder(\n                                this.state,\n                                (listenableContext) -> {\n                                    var buttons = new ArrayList<Widget>();\n                                    var state = this.state.value();\n\n                                    for (var idx = 0; idx < state.options().size(); idx++) {\n                                        var option = state.options().get(idx);\n\n                                        buttons.add(new HighlightableButton<>(\n                                            this.onSelect,\n                                            option,\n                                            idx == state.highlightedOptionIdx().orElse(-1),\n                                            this.optionToName\n                                        ));\n                                    }\n\n                                    return new Constrain(\n                                        Constraints.ofMaxHeight(13 * 8),\n                                        new ScrollableWithBars(\n                                            null,\n                                            null,\n                                            null,\n                                            4,\n                                            (layoutAxis, scrollController) -> new FlatScrollbar(layoutAxis, scrollController, Color.WHITE, Color.WHITE),\n                                            new Column(buttons)\n                                        )\n                                    );\n                                }\n                            )\n                        )\n                    )\n                )\n            )\n        );\n    }\n\n    private static class HighlightableButton<T> extends StatefulWidget {\n\n        public final ComboBox.SelectCallback<T> onSelect;\n        public final T option;\n        public final boolean highlighted;\n        public final Function<@Nullable T, Component> optionToName;\n\n        public HighlightableButton(ComboBox.SelectCallback<T> onSelect, T option, boolean highlighted, Function<@Nullable T, Component> optionToName) {\n            this.onSelect = onSelect;\n            this.option = option;\n            this.highlighted = highlighted;\n            this.optionToName = optionToName;\n        }\n\n        @Override\n        public WidgetState<HighlightableButton<T>> createState() {\n            return new State<>();\n        }\n\n        public static class State<T> extends WidgetState<HighlightableButton<T>> {\n\n            @Override\n            public void didUpdateWidget(HighlightableButton<T> oldWidget) {\n                if (!oldWidget.highlighted && this.widget().highlighted) {\n                    this.schedulePostLayoutCallback(() -> Scrollable.reveal(this.context()));\n                }\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Clickable(\n                    Clickable.alwaysClick(() -> this.widget().onSelect.onSelect(this.widget().option)),\n                    new HoverableBuilder(\n                        (hoverableContext, hovered) -> {\n                            var highlighted = hovered || this.widget().highlighted;\n\n                            return new Box(\n                                highlighted ? Color.WHITE.withA(.1f): new Color(0),\n                                new Padding(\n                                    Insets.all(2).withLeft(3),\n                                    new Label(\n                                        new LabelStyle(Alignment.LEFT, highlighted\n                                            ? Color.rgb(ChatFormatting.YELLOW.getColor()) : null, null, highlighted),\n                                        true,\n                                        this.widget().optionToName.apply(this.widget().option)\n                                    )\n                                )\n                            );\n                        }\n                    )\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/combobox/ComboBoxButtonsState.java",
    "content": "package io.wispforest.owo.braid.widgets.combobox;\n\nimport java.util.List;\nimport java.util.OptionalInt;\n\nrecord ComboBoxButtonsState<T>(List<T> options, OptionalInt highlightedOptionIdx) {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/cycle/Cycler.java",
    "content": "package io.wispforest.owo.braid.widgets.cycle;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.util.Mth;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class Cycler<T> extends StatelessWidget {\n    //Psyckler\n\n    public final List<T> values;\n    public final int currentIndex;\n\n    public final boolean wrap;\n    public final CyclerCallback<T> onChanged;\n\n    public final CyclingWidgetBuilder<T> builder;\n\n    public Cycler(List<T> values, T currentValue, boolean wrap, CyclerCallback<T> onChanged, CyclingWidgetBuilder<T> builder) {\n        this.values = values;\n        this.currentIndex = this.values.indexOf(currentValue);\n        this.wrap = wrap;\n        this.onChanged = onChanged;\n        this.builder = builder;\n    }\n\n    public Cycler(List<T> values, T currentValue, CyclerCallback<T> onChanged, CyclingWidgetBuilder<T> builder) {\n        this(values, currentValue, true, onChanged, builder);\n    }\n\n    public static Cycler<Boolean> forBoolean(boolean value, boolean wrap, CyclerCallback<Boolean> onChanged, CyclingWidgetBuilder<Boolean> builder) {\n        return new Cycler<>(List.of(false, true), value, wrap, onChanged, builder);\n    }\n\n    public static Cycler<Boolean> forBoolean(boolean value, CyclerCallback<Boolean> onChanged, CyclingWidgetBuilder<Boolean> builder) {\n        return Cycler.forBoolean(value, true, onChanged, builder);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Enum<T>> Cycler<T> forEnum(T value, boolean wrap, CyclerCallback<T> onChanged, CyclingWidgetBuilder<T> builder) {\n        return new Cycler<>((List<T>) Arrays.stream(value.getClass().getEnumConstants()).toList(), value, wrap, onChanged, builder);\n    }\n\n    public static <T extends Enum<T>> Cycler<T> forEnum(T value, CyclerCallback<T> onChanged, CyclingWidgetBuilder<T> builder) {\n        return Cycler.forEnum(value, true, onChanged, builder);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return this.builder.build(\n            this.values.get(this.currentIndex),\n            this.currentIndex,\n            amount -> {\n                var newIndex = this.wrap ? Mth.positiveModulo(this.currentIndex + amount, this.values.size()) : Mth.clamp(this.currentIndex + amount, 0, this.values.size() - 1);\n                if (newIndex == this.currentIndex) return false;\n                this.onChanged.cycle(this.values.get(newIndex), newIndex);\n                return true;\n            }\n        );\n    }\n\n    @FunctionalInterface\n    public interface CyclerCallback<T> {\n        void cycle(T newValue, int newIndex);\n    }\n\n    @FunctionalInterface\n    public interface CyclingWidgetBuilder<T> {\n        Widget build(T currentValue, int currentIndex, CycleFunction cycle);\n    }\n\n    @FunctionalInterface\n    public interface CycleFunction {\n        boolean cycle(int amount);\n\n        default boolean forScroll(double amount) {\n            if (amount == 0) return false;\n            return this.cycle(amount > 0 ? 1 : -1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/cycle/CyclingButton.java",
    "content": "package io.wispforest.owo.braid.widgets.cycle;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.ControlsOverride;\nimport io.wispforest.owo.braid.widgets.button.Button;\nimport io.wispforest.owo.braid.widgets.button.ButtonStyle;\nimport io.wispforest.owo.braid.widgets.button.DefaultButtonStyle;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\npublic class CyclingButton<T> extends StatelessWidget {\n\n    public final List<T> values;\n    public final T currentValue;\n    public final boolean wrap;\n    public final @Nullable Cycler.CyclerCallback<T> onChanged;\n    public final Widget child;\n\n    public CyclingButton(List<T> values, T currentValue, boolean wrap, @Nullable Cycler.CyclerCallback<T> onChanged, Widget child) {\n        this.values = values;\n        this.currentValue = currentValue;\n        this.wrap = wrap;\n        this.onChanged = onChanged;\n        this.child = child;\n    }\n\n    public CyclingButton(List<T> values, T currentValue, boolean wrap, Cycler.CyclerCallback<T> onChanged, boolean active, Widget child) {\n        this(values, currentValue, wrap, active ? onChanged : null, child);\n    }\n\n    public CyclingButton(List<T> values, T currentValue, @Nullable Cycler.CyclerCallback<T> onChanged, Widget child) {\n        this(values, currentValue, true, onChanged, child);\n    }\n\n    public CyclingButton(List<T> values, T currentValue, Cycler.CyclerCallback<T> onChanged, boolean active, Widget child) {\n        this(values, currentValue, true, active ? onChanged : null, child);\n    }\n\n    public static CyclingButton<Boolean> forBoolean(boolean value, @Nullable Cycler.CyclerCallback<Boolean> onChanged, Widget child) {\n        return new CyclingButton<>(List.of(false, true), value, true, onChanged, child);\n    }\n\n    public static CyclingButton<Boolean> forBoolean(boolean value, Cycler.CyclerCallback<Boolean> onChanged, boolean active, Widget child) {\n        return CyclingButton.forBoolean(value, active ? onChanged : null, child);\n    }\n\n    public static <T extends Enum<T>> CyclingButton<T> forEnum(T value, boolean wrap, @Nullable Cycler.CyclerCallback<T> onChanged, Widget child) {\n        return new CyclingButton<>(List.of(value.getDeclaringClass().getEnumConstants()), value, wrap, onChanged, child);\n    }\n\n    public static <T extends Enum<T>> CyclingButton<T> forEnum(T value, boolean wrap, Cycler.CyclerCallback<T> onChanged, boolean active, Widget child) {\n        return CyclingButton.forEnum(value, wrap, active ? onChanged : null, child);\n    }\n\n    public static <T extends Enum<T>> CyclingButton<T> forEnum(T value, @Nullable Cycler.CyclerCallback<T> onChanged, Widget child) {\n        return CyclingButton.forEnum(value, true, onChanged, child);\n    }\n\n    public static <T extends Enum<T>> CyclingButton<T> forEnum(T value, Cycler.CyclerCallback<T> onChanged, boolean active, Widget child) {\n        return CyclingButton.forEnum(value, true, onChanged, active, child);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        Widget content = this.child;\n        if (this.onChanged != null && !ControlsOverride.controlsDisabled(context)) {\n            // TODO: properly override the style once this is setupcallbackified\n            var clickSound = DefaultButtonStyle.maybeOf(context) instanceof ButtonStyle style\n                ? style.clickSound()\n                : null;\n\n            content = new Cycler<>(\n                this.values,\n                this.currentValue,\n                this.wrap,\n                this.onChanged,\n                (currentValue, currentIndex, cycle) -> {\n                    return new CyclingClickable(\n                        cycle,\n                        clickSound,\n                        true,\n                        new Button(\n                            () -> cycle.cycle(1),\n                            this.child\n                        )\n                    );\n                }\n            );\n        }\n\n        return content;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/cycle/CyclingClickable.java",
    "content": "package io.wispforest.owo.braid.widgets.cycle;\n\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.intents.*;\nimport io.wispforest.owo.ui.util.UISounds;\nimport net.minecraft.sounds.SoundEvent;\nimport net.minecraft.sounds.SoundEvents;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.ToIntFunction;\n\npublic class CyclingClickable extends StatelessWidget {\n\n    public final @Nullable Cycler.CycleFunction cycle;\n    public final @Nullable SoundEvent clickSound;\n    public final boolean skipFocusTraversal;\n    public final Widget child;\n\n    public CyclingClickable(@Nullable Cycler.CycleFunction cycle, @Nullable SoundEvent clickSound, boolean skipFocusTraversal, Widget child) {\n        this.cycle = cycle;\n        this.clickSound = clickSound;\n        this.skipFocusTraversal = skipFocusTraversal;\n        this.child = child;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        if (this.cycle == null) {\n            return this.child;\n        }\n\n        return new MouseArea(\n            widget ->\n                widget.scrollCallback((horizontal, vertical) -> this.cycle.forScroll(vertical)),\n            new Interactable(\n                SHORTCUTS,\n                widget -> widget\n                    .cursorStyle(CursorStyle.HAND)\n                    .skipTraversal(this.skipFocusTraversal)\n                    .addCallbackAction(\n                        AdjustIntent.class,\n                        this.cycleCallback(this.cycle, intent -> intent.direction().offset())\n                    ).addCallbackAction(\n                        PrimaryActionIntent.class,\n                        this.cycleCallback(this.cycle, intent -> 1)\n                    ).addCallbackAction(\n                        SecondaryActionIntent.class,\n                        this.cycleCallback(this.cycle, intent -> -1)\n                    ),\n                child\n            )\n        );\n    }\n\n    private <I extends Intent> Action.Callback<I> cycleCallback(Cycler.CycleFunction cycle, ToIntFunction<I> offset) {\n        var sound = this.clickSound != null ? this.clickSound : SoundEvents.UI_BUTTON_CLICK.value();\n        return (actionCtx, intent) -> {\n            if (cycle.cycle(offset.applyAsInt(intent))) {\n                UISounds.play(sound);\n            }\n        };\n    }\n\n    // ---\n\n    private static final Map<List<ShortcutTrigger>, Intent> SHORTCUTS = Map.of(\n        List.of(ShortcutTrigger.of(ShortcutTrigger.UP, ShortcutTrigger.RIGHT)), new AdjustIntent(AdjustIntent.Direction.INCREMENT),\n        List.of(ShortcutTrigger.of(ShortcutTrigger.RIGHT_CLICK, ShortcutTrigger.DOWN, ShortcutTrigger.LEFT)), new AdjustIntent(AdjustIntent.Direction.DECREMENT)\n    );\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/cycle/MessageCyclingButton.java",
    "content": "package io.wispforest.owo.braid.widgets.cycle;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\npublic class MessageCyclingButton<T> extends StatelessWidget {\n\n    public final List<T> values;\n    public final T currentValue;\n\n    public final boolean wrap;\n    public final Component text;\n    public final @Nullable Cycler.CyclerCallback<T> onChanged;\n\n    public MessageCyclingButton(List<T> values, T currentValue, boolean wrap, Component text, @Nullable Cycler.CyclerCallback<T> onChanged) {\n        this.values = values;\n        this.currentValue = currentValue;\n        this.wrap = wrap;\n        this.text = text;\n        this.onChanged = onChanged;\n    }\n\n    public MessageCyclingButton(List<T> values, T currentValue, boolean wrap, Component text, Cycler.CyclerCallback<T> onChanged, boolean active) {\n        this(values, currentValue, wrap, text, active ? onChanged : null);\n    }\n\n    public MessageCyclingButton(List<T> values, T currentValue, Component text, @Nullable Cycler.CyclerCallback<T> onChanged) {\n        this(values, currentValue, true, text, onChanged);\n    }\n\n    public MessageCyclingButton(List<T> values, T currentValue, Component text, Cycler.CyclerCallback<T> onChanged, boolean active) {\n        this(values, currentValue, true, text, onChanged, active);\n    }\n\n    public static MessageCyclingButton<Boolean> forBoolean(boolean value, Component text, @Nullable Cycler.CyclerCallback<Boolean> onChanged) {\n        return new MessageCyclingButton<>(List.of(false, true), value, true, text, onChanged);\n    }\n\n    public static MessageCyclingButton<Boolean> forBoolean(boolean value, Component text, Cycler.CyclerCallback<Boolean> onChanged, boolean active) {\n        return MessageCyclingButton.forBoolean(value, text, active ? onChanged : null);\n    }\n\n    public static <T extends Enum<T>> MessageCyclingButton<T> forEnum(T value, boolean wrap, Component text, @Nullable Cycler.CyclerCallback<T> onChanged) {\n        return new MessageCyclingButton<>(List.of(value.getDeclaringClass().getEnumConstants()), value, wrap, text, onChanged);\n    }\n\n    public static <T extends Enum<T>> MessageCyclingButton<T> forEnum(T value, boolean wrap, Component text, Cycler.CyclerCallback<T> onChanged, boolean active) {\n        return MessageCyclingButton.forEnum(value, wrap, text, active ? onChanged : null);\n    }\n\n    public static <T extends Enum<T>> MessageCyclingButton<T> forEnum(T value, Component text, @Nullable Cycler.CyclerCallback<T> onChanged) {\n        return MessageCyclingButton.forEnum(value, true, text, onChanged);\n    }\n\n    public static <T extends Enum<T>> MessageCyclingButton<T> forEnum(T value, Component text, Cycler.CyclerCallback<T> onChanged, boolean active) {\n        return MessageCyclingButton.forEnum(value, true, text, active ? onChanged : null);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new CyclingButton<>(\n            this.values,\n            this.currentValue,\n            this.wrap,\n            this.onChanged,\n            //TODO: abstract away the million places where a ternary operator is used to determine the label style for a possibly disabled button\n            new Label(\n                this.onChanged != null\n                    ? LabelStyle.SHADOW\n                    : new LabelStyle(null, Color.formatting(ChatFormatting.GRAY), null, false),\n                true,\n                this.text\n            )\n        );\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/drag/DragArena.java",
    "content": "package io.wispforest.owo.braid.widgets.drag;\n\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class DragArena extends MultiChildInstanceWidget {\n\n    public DragArena(List<? extends Widget> children) {\n        super(children);\n    }\n\n    public DragArena(Widget... children) {\n        this(Arrays.asList(children));\n    }\n\n    @Override\n    public MultiChildWidgetInstance<?> instantiate() {\n        return new DragArenaInstance(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/drag/DragArenaElement.java",
    "content": "package io.wispforest.owo.braid.widgets.drag;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.VisitorWidget;\n\npublic class DragArenaElement extends VisitorWidget {\n    public final double x, y;\n\n    public DragArenaElement(double x, double y, Widget child) {\n        super(child);\n        this.x = x;\n        this.y = y;\n    }\n\n    public static final Visitor<DragArenaElement> VISITOR = (widget, instance) -> {\n        if (instance.parentData instanceof DragParentData data) {\n            data.x = widget.x;\n            data.y = widget.y;\n        } else {\n            instance.parentData = new DragParentData(widget.x, widget.y);\n        }\n\n        instance.markNeedsLayout();\n    };\n\n    @Override\n    public Proxy<?> proxy() {\n        return new Proxy<>(this, VISITOR);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/drag/DragArenaInstance.java",
    "content": "package io.wispforest.owo.braid.widgets.drag;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\n\nimport java.util.OptionalDouble;\n\npublic class DragArenaInstance extends MultiChildWidgetInstance<DragArena> {\n\n    public DragArenaInstance(DragArena widget) {\n        super(widget);\n    }\n\n    @Override\n    public <W extends WidgetInstance<?>> W adopt(W child) {\n        if (child != null && !(child.parentData instanceof DragParentData)) {\n            child.parentData = new DragParentData(0, 0);\n        }\n\n        return super.adopt(child);\n    }\n\n    @Override\n    public void doLayout(Constraints constraints) {\n        for (var child : this.children) {\n            child.layout(Constraints.unconstrained());\n\n            var parentData = (DragParentData) child.parentData;\n            child.transform.setX(parentData.x);\n            child.transform.setY(parentData.y);\n        }\n\n        this.transform.setSize(constraints.maxSize());\n    }\n\n    @Override\n    protected double measureIntrinsicWidth(double height) {\n        return 0;\n    }\n\n    @Override\n    protected double measureIntrinsicHeight(double width) {\n        return 0;\n    }\n\n    @Override\n    protected OptionalDouble measureBaselineOffset() {\n        return this.computeHighestBaselineOffset();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/drag/DragParentData.java",
    "content": "package io.wispforest.owo.braid.widgets.drag;\n\npublic class DragParentData {\n    public double x, y;\n    public DragParentData(double x, double y) {\n        this.x = x;\n        this.y = y;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/eventstream/BraidEventSource.java",
    "content": "package io.wispforest.owo.braid.widgets.eventstream;\n\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\n\npublic class BraidEventSource<T> extends EventSource<BraidEventStream.Listener<T>> {\n    BraidEventSource(EventStream<BraidEventStream.Listener<T>> stream) {\n        super(stream);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/eventstream/BraidEventStream.java",
    "content": "package io.wispforest.owo.braid.widgets.eventstream;\n\nimport io.wispforest.owo.util.EventStream;\n\npublic class BraidEventStream<T> extends EventStream<BraidEventStream.Listener<T>> {\n\n    public BraidEventStream() {\n        super(listeners -> event -> {\n            for (var listener : listeners) listener.onEvent(event);\n        });\n    }\n\n    @Override\n    public BraidEventSource<T> source() {\n        return new BraidEventSource<>(this);\n    }\n\n    public interface Listener<T> {\n        void onEvent(T event);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/eventstream/StreamListenerState.java",
    "content": "package io.wispforest.owo.braid.widgets.eventstream;\n\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\npublic abstract class StreamListenerState<T extends StatefulWidget> extends WidgetState<T> {\n    private final List<SubscriptionData<T, ?>> streamSubscriptions = new ArrayList<>();\n\n    protected <S> void streamListen(Function<T, @Nullable BraidEventSource<S>> streamGetter, Consumer<S> onData) {\n        this.streamSubscriptions.add(\n            new SubscriptionData<>(this.widget(), streamGetter, stream -> stream.subscribe(onData::accept))\n        );\n    }\n\n    @Override\n    public void didUpdateWidget(T oldWidget) {\n        super.didUpdateWidget(oldWidget);\n\n        for (var subscription : this.streamSubscriptions) {\n            subscription.update(this.widget());\n        }\n    }\n\n    @Override\n    public void dispose() {\n        super.dispose();\n\n        for (var subscription : this.streamSubscriptions) {\n            if (subscription.currentSubscription != null) subscription.currentSubscription.cancel();\n        }\n    }\n\n\n    private static class SubscriptionData<W, T> {\n        private final Function<W, @Nullable BraidEventSource<T>> getter;\n        private final Function<BraidEventSource<T>, BraidEventSource<T>.Subscription> listenerFactory;\n\n        private @Nullable BraidEventSource<T> currentStream;\n        private @Nullable BraidEventSource<T>.Subscription currentSubscription;\n\n        private SubscriptionData(W widget, Function<W, @Nullable BraidEventSource<T>> getter, Function<BraidEventSource<T>, BraidEventSource<T>.Subscription> listenerFactory) {\n            this.getter = getter;\n            this.listenerFactory = listenerFactory;\n\n            this.listenOn(widget);\n        }\n\n        private void listenOn(W widget) {\n            this.currentStream = this.getter.apply(widget);\n            if (this.currentStream == null) return;\n\n            this.currentSubscription = this.listenerFactory.apply(this.currentStream);\n        }\n\n        public void update(W newWidget) {\n            var newStream = this.getter.apply(newWidget);\n            if (newStream == this.currentStream) return;\n\n            if (this.currentSubscription != null) this.currentSubscription.cancel();\n            listenOn(newWidget);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/Column.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Arrays;\nimport java.util.List;\n\n/// A [Flex], restricted to the vertical axis.\n///\n/// See the [Flex] documentation for details\npublic class Column extends Flex {\n    public Column(\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        @Nullable Widget separator,\n        List<? extends Widget> children\n    ) {\n        super(LayoutAxis.VERTICAL, mainAxisAlignment, crossAxisAlignment, separator, children);\n    }\n\n    /// Create a column without a separator\n    public Column(\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        List<? extends Widget> children\n    ) {\n        this(mainAxisAlignment, crossAxisAlignment, null, children);\n    }\n\n    /// Create a column without a separator\n    public Column(\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        Widget... children\n    ) {\n        this(mainAxisAlignment, crossAxisAlignment, null, Arrays.asList(children));\n    }\n\n    /// Create a column with default (start) alignment\n    /// on both axes\n    public Column(\n        @Nullable Widget separator,\n        List<? extends Widget> children\n    ) {\n        this(MainAxisAlignment.START, CrossAxisAlignment.START, separator, children);\n    }\n\n    /// Create a column with default (start) alignment\n    /// on both axes and no separator\n    public Column(\n        List<? extends Widget> children\n    ) {\n        this(MainAxisAlignment.START, CrossAxisAlignment.START, null, children);\n    }\n\n    /// Create a column with default (start) alignment\n    /// on both axes and no separator\n    public Column(\n        Widget... children\n    ) {\n        this(Arrays.asList(children));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/CrossAxisAlignment.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\npublic enum CrossAxisAlignment {\n    /// The start of the cross axis (left for a column, top for a row)\n    START,\n\n    /// The end of the cross axis (right for a column, bottom for a row)\n    END,\n\n    /// Center across the cross axis\n    CENTER,\n\n    /// Force all children to fill the flex's cross axis constraints\n    STRETCH;\n\n    @SuppressWarnings(\"DuplicateBranchesInSwitch\")\n    double computeChildOffset(double freeSpace) {\n        return Math.floor(switch (this) {\n            case STRETCH -> 0;\n            case START -> 0;\n            case CENTER -> freeSpace / 2;\n            case END -> freeSpace;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/Flex.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.util.Util;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/// A Flex places its children one after another in a horizontal\n/// or vertical list, as dictated by its [#mainAxis].\n///\n/// Child alignment on both axes is given by [#mainAxisAlignment]\n/// and [#crossAxisAlignment] respectively.\n///\n/// Children receive loose and unbounded constraints on the main axis\n/// and the Flex's cross axis constraints are passed down unchanged. If\n/// cross axis alignment is set to [CrossAxisAlignment#STRETCH], cross\n/// axis constraints are tightened to their maximum.\n///\n/// Children can be made to receive tight main axis constraints by wrapping\n/// them in a [Flexible]. After all non-flexible children are laid out,\n/// the remaining main axis space is divided up amongst all [Flexible] children,\n/// weighted by their respective [Flexible#flexFactor].\n///\n/// A Flex can have an optional `separator`, which is inserted between each child\npublic class Flex extends MultiChildInstanceWidget {\n\n    /// The axis along which the children of this widget\n    /// are placed\n    public final LayoutAxis mainAxis;\n\n    /// How remaining space on the main axis, if any,\n    /// should be distributed between and around the children\n    public final MainAxisAlignment mainAxisAlignment;\n\n    /// How remaining space on the cross axis, if any,\n    /// should be distributed around the children\n    public final CrossAxisAlignment crossAxisAlignment;\n\n    public Flex(\n        LayoutAxis mainAxis,\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        @Nullable Widget separator,\n        List<? extends Widget> children\n    ) {\n        super(Util.make(() -> {\n            if (separator == null || children.size() < 2) return children;\n\n            var result = new ArrayList<Widget>();\n            for (var i = 0; i < children.size() - 1; i++) {\n                result.add(children.get(i));\n                result.add(separator);\n            }\n\n            result.add(children.getLast());\n            return result;\n        }));\n        this.mainAxis = mainAxis;\n        this.mainAxisAlignment = mainAxisAlignment;\n        this.crossAxisAlignment = crossAxisAlignment;\n    }\n\n    /// Create a flex without a separator\n    public Flex(\n        LayoutAxis mainAxis,\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        Widget... children\n    ) {\n        this(mainAxis, mainAxisAlignment, crossAxisAlignment, null, Arrays.asList(children));\n    }\n\n    @Override\n    public MultiChildWidgetInstance<?> instantiate() {\n        return new FlexInstance(this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/FlexInstance.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\nimport com.google.common.collect.Iterables;\nimport com.google.common.collect.Streams;\nimport io.wispforest.owo.braid.core.BraidUtils;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\n\nimport java.util.OptionalDouble;\nimport java.util.stream.Collectors;\n\npublic class FlexInstance extends MultiChildWidgetInstance<Flex> {\n\n    public FlexInstance(Flex widget) {\n        super(widget);\n    }\n\n    @Override\n    public void setWidget(Flex widget) {\n        if (this.widget.mainAxis == widget.mainAxis\n            && this.widget.mainAxisAlignment == widget.mainAxisAlignment\n            && this.widget.crossAxisAlignment == widget.crossAxisAlignment) {\n            return;\n        }\n\n        super.setWidget(widget);\n        this.markNeedsLayout();\n    }\n\n    @Override\n    protected void doLayout(Constraints constraints) {\n        var mainAxis = widget.mainAxis;\n        var crossAxis = mainAxis.opposite();\n\n        var crossAxisMinimum =\n            widget.crossAxisAlignment == CrossAxisAlignment.STRETCH\n                ? constraints.maxOnAxis(crossAxis)\n                : constraints.minOnAxis(crossAxis);\n\n        var childConstraints = Constraints.of(\n            mainAxis == LayoutAxis.VERTICAL ? crossAxisMinimum : 0,\n            mainAxis == LayoutAxis.HORIZONTAL ? crossAxisMinimum : 0,\n            mainAxis == LayoutAxis.VERTICAL ? constraints.maxOnAxis(crossAxis) : Double.POSITIVE_INFINITY,\n            mainAxis == LayoutAxis.HORIZONTAL ? constraints.maxOnAxis(crossAxis) : Double.POSITIVE_INFINITY\n        );\n\n        // first, lay out all non-flex children and store their sizes\n        var childSizes = this.children.stream()\n            .filter(element -> !(element.parentData instanceof FlexParentData))\n            .map((e) -> e.layout(childConstraints))\n            .collect(Collectors.toList());\n\n        // now, compute the remaining space on the main axis\n        var remainingSpace = Math.max(\n            constraints.maxOnAxis(mainAxis) - BraidUtils.fold(childSizes, 0.0, (acc, size) -> acc + size.getExtent(mainAxis)),\n            0\n        );\n\n        // get the flex children and compute the total flex factor in order\n        // to divvy up the remaining space properly later\n        var flexChildren = Iterables.filter(children, (element) -> element.parentData instanceof FlexParentData);\n        var totalFlexFactor = BraidUtils.fold(\n            flexChildren,\n            0.0,\n            (previousValue, element) -> previousValue + ((FlexParentData) element.parentData).flexFactor\n        );\n\n        // lay out all flex children with (for now) tight constraints\n        // on the main axis according to their allotted space\n        for (var child : flexChildren) {\n            var space = remainingSpace * (((FlexParentData) child.parentData).flexFactor / totalFlexFactor);\n            childSizes.add(\n                child.layout(\n                    childConstraints.respecting(\n                        Constraints.tightOnAxis(\n                            mainAxis == LayoutAxis.HORIZONTAL ? space : null,\n                            mainAxis == LayoutAxis.VERTICAL ? space : null\n                        )\n                    )\n                )\n            );\n        }\n\n        // compute and apply the final size of ourselves\n        var size = BraidUtils.fold(\n            childSizes,\n            Size.zero(),\n            (acc, elem) -> mainAxis.createSize(\n                acc.getExtent(mainAxis) + elem.getExtent(mainAxis),\n                Math.max(acc.getExtent(crossAxis), elem.getExtent(crossAxis))\n            )\n        ).constrained(constraints);\n\n        this.transform.setSize(size);\n\n        // distribute remaining space on the main axis\n        var freeSpace = size.getExtent(mainAxis) - BraidUtils.fold(childSizes, 0.0, (acc, elem) -> acc + elem.getExtent(mainAxis));\n\n        var leadingSpace = this.widget.mainAxisAlignment.leadingSpace(freeSpace, childSizes.size());\n        var betweenSpace = this.widget.mainAxisAlignment.between(freeSpace, childSizes.size());\n\n        // move children into position and apply cross-axis alignment\n        var mainAxisOffset = leadingSpace;\n        for (var child : children) {\n            child.transform.setCoordinate(mainAxis, mainAxisOffset);\n\n            child.transform.setCoordinate(\n                crossAxis,\n                this.widget.crossAxisAlignment.computeChildOffset(\n                    size.getExtent(crossAxis) - child.transform.getExtent(crossAxis)\n                )\n            );\n\n            mainAxisOffset += child.transform.getExtent(mainAxis) + betweenSpace;\n        }\n    }\n\n    @Override\n    protected double measureIntrinsicWidth(double height) {\n        return this.widget.mainAxis == LayoutAxis.HORIZONTAL ? this.measureMainAxis(height) : this.measureCrossAxis(height);\n    }\n\n    @Override\n    protected double measureIntrinsicHeight(double width) {\n        return this.widget.mainAxis == LayoutAxis.VERTICAL ? this.measureMainAxis(width) : this.measureCrossAxis(width);\n    }\n\n    @Override\n    protected OptionalDouble measureBaselineOffset() {\n        return switch (this.widget.mainAxis) {\n            case VERTICAL -> this.computeFirstBaselineOffset();\n            case HORIZONTAL -> this.computeHighestBaselineOffset();\n        };\n    }\n\n    @SuppressWarnings(\"DataFlowIssue\")\n    private double measureMainAxis(double crossExtent) {\n        var horizontal = this.widget.mainAxis == LayoutAxis.HORIZONTAL;\n        var nonFlexSize = this.children.stream()\n            .filter(element -> !(element.parentData instanceof FlexParentData))\n            .mapToDouble(e -> horizontal ? e.getIntrinsicWidth(crossExtent) : e.getIntrinsicHeight(crossExtent))\n            .sum();\n\n        var totalFlexFactor = 0.0;\n\n        WidgetInstance<?> largestFlexChild = null;\n        double largestFlexChildSize = 0;\n        double largestFlexChildFlexFactor = 0;\n\n        for (var flexChild : Iterables.filter(this.children, element -> element.parentData instanceof FlexParentData)) {\n            totalFlexFactor += ((FlexParentData) flexChild.parentData).flexFactor;\n\n            var size = horizontal ? flexChild.getIntrinsicWidth(crossExtent) : flexChild.getIntrinsicHeight(crossExtent);\n            if (size > largestFlexChildSize) {\n                largestFlexChild = flexChild;\n                largestFlexChildSize = size;\n                largestFlexChildFlexFactor = ((FlexParentData) flexChild.parentData).flexFactor;\n            }\n        }\n\n        var flexSize = largestFlexChild != null ? (totalFlexFactor / largestFlexChildFlexFactor) * largestFlexChildSize : 0;\n\n        return nonFlexSize + flexSize;\n    }\n\n    @SuppressWarnings(\"DataFlowIssue\")\n    private double measureCrossAxis(double mainExtent) {\n        var horizontal = this.widget.mainAxis == LayoutAxis.HORIZONTAL;\n\n        var crossSize = 0.0;\n\n        var nonFlexSize = 0.0;\n        for (var child : Iterables.filter(this.children, element -> !(element.parentData instanceof FlexParentData))) {\n            var childSize = horizontal ? child.getIntrinsicHeight(mainExtent) : child.getIntrinsicWidth(mainExtent);\n\n            nonFlexSize += childSize;\n            crossSize = Math.max(crossSize, childSize);\n        }\n\n        var flexChildren = Iterables.filter(children, (element) -> element.parentData instanceof FlexParentData);\n        var totalFlexFactor = Streams.stream(flexChildren).mapToDouble(e -> ((FlexParentData) e.parentData).flexFactor).sum();\n\n        for (var child : flexChildren) {\n            var childSpace = (mainExtent - nonFlexSize) * (totalFlexFactor / ((FlexParentData) child.parentData).flexFactor);\n\n            crossSize = Math.max(\n                crossSize,\n                horizontal ? child.getIntrinsicHeight(childSpace) : child.getIntrinsicWidth(childSpace)\n            );\n        }\n\n        return crossSize;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/FlexParentData.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\npublic class FlexParentData {\n    public double flexFactor;\n    public FlexParentData(double flexFactor) {\n        this.flexFactor = flexFactor;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/Flexible.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.VisitorWidget;\n\n/// A widget which is forced to fill the remaining space in its\n/// [Flex] parent. The space is weighted between all Flexible\n/// children according to their [#flexFactor]\n///\n/// Check the [Flex] documentation for details\n///\n/// **Note:** For Flexible to work, it is essential that the path from it to\n/// its enclosing [Flex] contains only stateful and stateless widgets.\npublic class Flexible extends VisitorWidget {\n\n    /// The relative proportion of the parent's\n    /// remaining space to assign to this widget\n    public final double flexFactor;\n\n    public Flexible(double flexFactor, Widget child) {\n        super(child);\n        this.flexFactor = flexFactor;\n    }\n\n    /// Create a Flexible with the default flex factor 1\n    public Flexible(Widget child) {\n        this(1, child);\n    }\n\n    private static final Visitor<Flexible> VISITOR = (widget, instance) -> {\n        if (instance.parentData instanceof FlexParentData data) {\n            data.flexFactor = widget.flexFactor;\n        } else {\n            instance.parentData = new FlexParentData(widget.flexFactor);\n        }\n\n        instance.markNeedsLayout();\n    };\n\n    @Override\n    public Proxy<?> proxy() {\n        return new VisitorWidget.Proxy<>(this, VISITOR);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/MainAxisAlignment.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\npublic enum MainAxisAlignment {\n    /// The start of the main axis (top for a column, left for a row)\n    START,\n\n    /// The end of the main axis (bottom for a column, right for a row)\n    END,\n\n    /// Center in the main axis\n    CENTER,\n\n    /// Distribute any remaining space evenly between all children\n    SPACE_BETWEEN,\n\n    /// Distribute half of any remaining space equally before the first and\n    /// after the last child, and the other half evenly between all children\n    SPACE_AROUND,\n\n    /// Distribute any remaining space evenly between all children\n    /// as well as before the first and after the last child\n    SPACE_EVENLY;\n\n    @SuppressWarnings(\"DuplicateBranchesInSwitch\")\n    double leadingSpace(double freeSpace, int childCount) {\n        return Math.floor(switch (this) {\n            case START -> 0;\n            case END -> freeSpace;\n            case CENTER -> freeSpace / 2;\n            case SPACE_BETWEEN -> 0;\n            case SPACE_AROUND -> freeSpace / childCount / 2;\n            case SPACE_EVENLY -> freeSpace / (childCount + 1);\n        });\n    }\n\n    @SuppressWarnings(\"DuplicateBranchesInSwitch\")\n    double between(double freeSpace, int childCount) {\n        return Math.floor(switch (this) {\n            case START -> 0;\n            case END -> 0;\n            case CENTER -> 0;\n            case SPACE_BETWEEN -> freeSpace / (childCount - 1);\n            case SPACE_AROUND -> freeSpace / childCount;\n            case SPACE_EVENLY -> freeSpace / (childCount + 1);\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/flex/Row.java",
    "content": "package io.wispforest.owo.braid.widgets.flex;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Arrays;\nimport java.util.List;\n\n/// A [Flex], restricted to the horizontal axis.\n///\n/// See the [Flex] documentation for details\npublic class Row extends Flex {\n    public Row(\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        @Nullable Widget separator,\n        List<? extends Widget> children\n    ) {\n        super(LayoutAxis.HORIZONTAL, mainAxisAlignment, crossAxisAlignment, separator, children);\n    }\n\n    /// Create a row without a separator\n    public Row(\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        List<? extends Widget> children\n    ) {\n        this(mainAxisAlignment, crossAxisAlignment, null, children);\n    }\n\n    /// Create a row without a separator\n    public Row(\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        Widget... children\n    ) {\n        this(mainAxisAlignment, crossAxisAlignment, null, Arrays.asList(children));\n    }\n\n    /// Create a row with default (start) alignment\n    /// on both axes\n    public Row(\n        @Nullable Widget separator,\n        List<? extends Widget> children\n    ) {\n        this(MainAxisAlignment.START, CrossAxisAlignment.START, separator, children);\n    }\n\n    /// Create a column with default (start) alignment\n    /// on both axes and no separator\n    public Row(\n        List<? extends Widget> children\n    ) {\n        this(MainAxisAlignment.START, CrossAxisAlignment.START, null, children);\n    }\n\n    /// Create a column with default (start) alignment\n    /// on both axes and no separator\n    public Row(\n        Widget... children\n    ) {\n        this(Arrays.asList(children));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/FocusClickArea.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class FocusClickArea extends SingleChildInstanceWidget {\n\n    public final Runnable clickCallback;\n\n    public FocusClickArea(Runnable clickCallback, Widget child) {\n        super(child);\n        this.clickCallback = clickCallback;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<FocusClickArea> {\n        public Instance(FocusClickArea widget) {\n            super(widget);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/FocusLevel.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\npublic enum FocusLevel {\n    BASE,\n    HIGHLIGHT\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/FocusPolicy.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class FocusPolicy extends InheritedWidget {\n\n    public final boolean clickFocus;\n\n    public FocusPolicy(boolean clickFocus, Widget child) {\n        super(child);\n        this.clickFocus = clickFocus;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return ((FocusPolicy) newWidget).clickFocus != this.clickFocus;\n    }\n\n    // ---\n\n    public static FocusPolicy of(BuildContext context) {\n        return context.dependOnAncestor(FocusPolicy.class);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/FocusScope.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.StatefulProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.scroll.Scrollable;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport net.minecraft.util.Mth;\nimport net.minecraft.world.phys.AABB;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\n\nimport java.util.*;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class FocusScope extends Focusable {\n\n    public FocusScope(WidgetSetupCallback<FocusScope> setupCallback, Widget child) {\n        super(widget -> setupCallback.setup((FocusScope) widget), child);\n    }\n\n    @Override\n    public WidgetProxy proxy() {\n        return new FocusScopeProxy(this);\n    }\n\n    @Override\n    public WidgetState<FocusScope> createState() {\n        return new State();\n    }\n\n    public static class State extends Focusable.State<FocusScope> {\n\n        Supplier<List<Focusable.State<?>>> descendants;\n\n        private List<Focusable.State<?>> focusedDescendants = new ArrayList<>();\n        private @Nullable FocusEntry previousPrimaryFocus;\n        private final Deque<FocusEntry> previouslyFocusedScopes = new ArrayDeque<>();\n\n        private final Deque<Focusable.State<?>> traversalHistory = new LinkedList<>();\n        private FocusTraversalDirection historyDirection = null;\n\n        public void updateFocus(@Nullable Focusable.State<?> primary, @Nullable FocusLevel level) {\n            this.updateFocus(primary, level, false);\n        }\n\n        public void updateFocus(@Nullable Focusable.State<?> primary, @Nullable FocusLevel level, boolean keepTraversalHistory) {\n            var currentPrimaryFocus = !this.focusedDescendants.isEmpty() ? this.focusedDescendants.getFirst() : null;\n            if (primary == currentPrimaryFocus && (primary != null ? primary.level : null) == level) {\n                return;\n            }\n\n            if (!keepTraversalHistory) {\n                this.traversalHistory.clear();\n            }\n\n            if (level != null && primary != null) {\n                this.requestFocus(level);\n            }\n\n            var nowFocused = primary != null\n                ? Stream.concat(Stream.of(primary), primary.ancestors()).takeWhile(state -> state != this).collect(Collectors.toList())\n                : new ArrayList<Focusable.State<?>>();\n\n            for (var state : nowFocused) {\n                if (this.focusedDescendants.contains(state)) {\n                    this.focusedDescendants.remove(state);\n\n                    if (state.level != level) {\n                        state.onFocusChange(level);\n                    }\n                } else {\n                    state.onFocusChange(level);\n                }\n            }\n\n            if (!this.focusedDescendants.isEmpty() && this.focusedDescendants.getFirst() instanceof State scope && !nowFocused.contains(scope)) {\n                this.previouslyFocusedScopes.add(new FocusEntry(scope, scope.level));\n            } else if (nowFocused.isEmpty() || !(nowFocused.getFirst() instanceof State)) {\n                previouslyFocusedScopes.clear();\n            }\n\n            for (var noLongerFocused : this.focusedDescendants) {\n                noLongerFocused.onFocusChange(null);\n            }\n\n            if (primary != null) {\n                var scrollable = Scrollable.maybeOf(primary.context());\n                if (scrollable != null) {\n                    Scrollable.reveal(primary.context());\n                }\n            }\n\n            this.focusedDescendants = nowFocused;\n        }\n\n        void onFocusableDisposed(Focusable.State<?> descendant) {\n            if (!this.focusedDescendants.isEmpty() && descendant == this.focusedDescendants.getFirst() && !this.previouslyFocusedScopes.isEmpty()) {\n                var entry = this.previouslyFocusedScopes.removeLast();\n                updateFocus(entry.state(), entry.level());\n            }\n\n            this.focusedDescendants.remove(descendant);\n            this.traversalHistory.remove(descendant);\n            this.previouslyFocusedScopes.removeIf(entry -> entry.state() == descendant);\n        }\n\n        @Override\n        public Focusable.State<?> primaryFocus() {\n            if (this.level != null) {\n                var candidate = !this.focusedDescendants.isEmpty() ? this.focusedDescendants.getFirst() : null;\n                if (candidate instanceof State) candidate = candidate.primaryFocus();\n\n                return candidate != null ? candidate : this;\n            } else {\n                return super.primaryFocus();\n            }\n        }\n\n        @Override\n        public void traverseFocus(FocusTraversalDirection direction) {\n            switch (direction) {\n                case PREVIOUS, NEXT -> this.traverseFocusLogical(direction == FocusTraversalDirection.NEXT);\n                case LEFT, RIGHT, UP, DOWN -> this.traverseFocusDirectional(direction);\n            }\n        }\n\n        private void traverseFocusLogical(boolean forwards) {\n            var descendants = this.descendants.get();\n\n            var searchStartIdx = !this.focusedDescendants.isEmpty()\n                ? descendants.indexOf(this.focusedDescendants.getFirst())\n                : (forwards ? -1 : 0);\n            var offset = forwards ? 1 : -1;\n\n            var nextFocusIdx = searchStartIdx;\n            do {\n                nextFocusIdx = Mth.positiveModulo(nextFocusIdx + offset, descendants.size());\n            } while (descendants.get(nextFocusIdx).widget().skipTraversal());\n\n            this.updateFocus(descendants.get(nextFocusIdx), FocusLevel.HIGHLIGHT);\n        }\n\n        private boolean tryTraverseFocusHistory(FocusTraversalDirection direction) {\n            var poppedHistory = false;\n\n            if (!this.traversalHistory.isEmpty()) {\n                if (this.historyDirection == direction.opposite()) {\n                    poppedHistory = true;\n                    this.updateFocus(this.traversalHistory.pop(), FocusLevel.HIGHLIGHT, true);\n                } else if (this.historyDirection != direction) {\n                    this.traversalHistory.clear();\n                }\n            }\n\n            if (!poppedHistory && !this.focusedDescendants.isEmpty()) {\n                this.historyDirection = direction;\n            }\n\n            return poppedHistory;\n        }\n\n        private void traverseFocusDirectional(FocusTraversalDirection direction) {\n            if (this.focusedDescendants.isEmpty() || this.tryTraverseFocusHistory(direction)) return;\n\n            var descendants = this.descendants.get();\n\n            var focusedBounds = this.focusedDescendants.getFirst().context().instance().computeGlobalBounds();\n            var focusedCenter = FocusTraversalCandidate.of(this.focusedDescendants.getFirst()).center();\n\n            var candidates = descendants.stream()\n                .filter(state -> !state.widget().skipTraversal())\n                .map(FocusTraversalCandidate::of)\n                .filter(candidate -> {\n                    return this.filterCandidate(candidate, focusedBounds, direction);\n                })\n                .collect(Collectors.toList());\n\n            var candidatesInBand = candidates.stream()\n                .filter(candidate -> {\n                    return this.filterInBand(candidate, focusedBounds, direction);\n                })\n                .collect(Collectors.toList());\n\n            if (!candidatesInBand.isEmpty()) {\n                candidatesInBand.sort(this.sortInBand(focusedCenter, direction));\n\n                this.traversalHistory.push(this.focusedDescendants.getFirst());\n                this.updateFocus(candidatesInBand.getFirst().state(), FocusLevel.HIGHLIGHT, true);\n                return;\n            }\n\n            if (!candidates.isEmpty()) {\n                candidates.sort(this.sortOutOfBand(focusedCenter, direction));\n\n                this.traversalHistory.push(this.focusedDescendants.getFirst());\n                this.updateFocus(candidates.getFirst().state(), FocusLevel.HIGHLIGHT, true);\n            }\n        }\n\n        private boolean filterCandidate(FocusTraversalCandidate candidate, AABB focusedBounds, FocusTraversalDirection direction) {\n            return switch (direction) {\n                case LEFT -> candidate.center().x <= focusedBounds.minX;\n                case RIGHT -> candidate.center().x >= focusedBounds.maxX;\n                case UP -> candidate.center().y <= focusedBounds.minY;\n                case DOWN -> candidate.center().y >= focusedBounds.maxY;\n                default -> throw new IllegalStateException();\n            };\n        }\n\n        private boolean filterInBand(FocusTraversalCandidate candidate, AABB focusedBounds, FocusTraversalDirection direction) {\n            return switch (direction) {\n                case LEFT, RIGHT -> candidate.aabb().minY < focusedBounds.maxY && candidate.aabb().maxY > focusedBounds.minY;\n                case UP, DOWN -> candidate.aabb().minX < focusedBounds.maxX && candidate.aabb().maxX > focusedBounds.minX;\n                default -> throw new IllegalStateException();\n            };\n        }\n\n        private Comparator<FocusTraversalCandidate> sortInBand(Vector2d focusedCenter, FocusTraversalDirection direction) {\n            return switch (direction) {\n                case LEFT -> Comparator.<FocusTraversalCandidate>comparingDouble(candidate -> -candidate.center().x)\n                    .thenComparingDouble(candidate -> Math.abs(candidate.center().y - focusedCenter.y));\n                case RIGHT -> Comparator.<FocusTraversalCandidate>comparingDouble(candidate -> candidate.center().x)\n                    .thenComparingDouble(candidate -> Math.abs(candidate.center().y - focusedCenter.y));\n                case UP -> Comparator.<FocusTraversalCandidate>comparingDouble(candidate -> -candidate.center().y)\n                    .thenComparingDouble(candidate -> Math.abs(candidate.center().x - focusedCenter.x));\n                case DOWN -> Comparator.<FocusTraversalCandidate>comparingDouble(candidate -> candidate.center().y)\n                    .thenComparingDouble(candidate -> Math.abs(candidate.center().x - focusedCenter.x));\n                default -> throw new IllegalStateException();\n            };\n        }\n\n        private Comparator<FocusTraversalCandidate> sortOutOfBand(Vector2d focusedCenter, FocusTraversalDirection direction) {\n            return switch (direction) {\n                case LEFT, RIGHT -> Comparator.<FocusTraversalCandidate>comparingDouble(candidate -> Math.abs(candidate.center().y - focusedCenter.y))\n                    .thenComparingDouble(candidate -> Math.abs(candidate.center().x - focusedCenter.x));\n                case UP, DOWN -> Comparator.<FocusTraversalCandidate>comparingDouble(candidate -> Math.abs(candidate.center().x - focusedCenter.x))\n                    .thenComparingDouble(candidate -> Math.abs(candidate.center().y - focusedCenter.y));\n                default -> throw new IllegalStateException();\n            };\n        }\n\n        @Override\n        void onFocusChange(@Nullable FocusLevel newLevel) {\n            var previousLevel = this.level;\n            super.onFocusChange(newLevel);\n\n            if (previousLevel != null && newLevel == null) {\n                var primaryFocus = !this.focusedDescendants.isEmpty() ? this.focusedDescendants.getFirst() : null;\n                this.previousPrimaryFocus = primaryFocus != null ? new FocusEntry(primaryFocus, primaryFocus.level) : null;\n\n                this.updateFocus(null, null);\n            } else if (previousLevel == null && newLevel != null && this.previousPrimaryFocus != null) {\n                this.updateFocus(this.previousPrimaryFocus.state(), this.previousPrimaryFocus.level());\n            }\n        }\n\n        @Override\n        boolean onKeyDown(int keyCode, KeyModifiers modifiers) {\n            for (var descendant : this.focusedDescendants) {\n                if (descendant.onKeyDown(keyCode, modifiers)) {\n                    return true;\n                }\n            }\n\n            return super.onKeyDown(keyCode, modifiers);\n        }\n\n        @Override\n        boolean onKeyUp(int keyCode, KeyModifiers modifiers) {\n            for (var descendant : this.focusedDescendants) {\n                if (descendant.onKeyUp(keyCode, modifiers)) {\n                    return true;\n                }\n            }\n\n            return super.onKeyUp(keyCode, modifiers);\n        }\n\n        @Override\n        boolean onChar(int charCode, KeyModifiers modifiers) {\n            for (var descendant : this.focusedDescendants) {\n                if (descendant.onChar(charCode, modifiers)) {\n                    return true;\n                }\n            }\n\n            return super.onChar(charCode, modifiers);\n        }\n\n        @Override\n        void onClick() {\n            super.onClick();\n            this.updateFocus(null, null);\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Stack(\n                new StackBase(\n                    new FocusStateProvider<>(\n                        this,\n                        State.class,\n                        this.level,\n                        super.build(context)\n                    )\n                )\n//                new CustomDraw((ctx, transform) -> {\n//                    if (this.focusedDescendants.isEmpty()) return;\n//\n//                    var instance = this.focusedDescendants.getFirst().context().instance();\n//                    var drawTransform = instance.parent().computeTransformFrom(this.context().instance()).invert();\n//\n//                    var boxMin = drawTransform.transformPosition(instance.transform.aabb().getMinPos().toVector3f());\n//                    var boxMax = drawTransform.transformPosition(instance.transform.aabb().getMaxPos().toVector3f());\n//\n//                    var box = new Box(new Vec3d(boxMin), new Vec3d(boxMax));\n//\n//                    ctx.push();\n//                    ctx.translate(box.minX, box.minY, box.minZ);\n//\n//                    NinePatchTexture.draw(\n//                        Identifier.of(\"owo\", \"braid_debug_focused\"),\n//                        ctx,\n//                        0, 0, (int) (box.maxX - box.minX), (int) (box.maxY - box.minY),\n//                        Color.ofHsv(this.focusedDescendants.getFirst().debugDepth() / 8f % 1f, .75f, 1)\n//                    );\n//\n//                    ctx.pop();\n//                })\n            );\n        }\n\n        // ---\n\n        static @Nullable FocusScope.State maybeOf(BuildContext context) {\n            var provider = context.getAncestor(FocusStateProvider.class, FocusStateProvider.keyOf(State.class));\n            if (provider == null) return null;\n\n            return (State) provider.state;\n        }\n    }\n}\n\nclass FocusScopeProxy extends StatefulProxy {\n    public FocusScopeProxy(FocusScope widget) {\n        super(widget);\n    }\n\n    @Override\n    public void mount(WidgetProxy parent, @Nullable Object slot) {\n        super.mount(parent, slot);\n        ((FocusScope.State) this.state()).descendants = () -> {\n            var descendants = new ArrayList<Focusable.State<?>>();\n            this.visitChildren(child -> collectFocusDescendants(child, descendants));\n\n            return descendants;\n        };\n    }\n\n    private static void collectFocusDescendants(WidgetProxy proxy, List<Focusable.State<?>> into) {\n        if (proxy instanceof StatefulProxy stateful && stateful.state() instanceof Focusable.State<?> state) {\n            into.add(state);\n\n            if (state instanceof FocusScope.State) {\n                return;\n            }\n        }\n\n        proxy.visitChildren(child -> {\n            collectFocusDescendants(child, into);\n        });\n    }\n}\n\nrecord FocusEntry(Focusable.State<?> state, FocusLevel level) {}\n\nrecord FocusTraversalCandidate(Focusable.State<?> state, AABB aabb, Vector2d center) {\n    public static FocusTraversalCandidate of(Focusable.State<?> state) {\n        var aabb = state.context().instance().computeGlobalBounds();\n        var center = new Vector2d(\n            aabb.minX + (aabb.maxX - aabb.minX) / 2,\n            aabb.minY + (aabb.maxY - aabb.minY) / 2\n        );\n\n        return new FocusTraversalCandidate(state, aabb, center);\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/FocusStateProvider.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nclass FocusStateProvider<F extends Focusable.State<?>> extends InheritedWidget {\n\n    public final F state;\n    public final @Nullable FocusLevel level;\n\n    private final InheritedKey inheritedKey;\n\n    public FocusStateProvider(F state, Class<F> stateClass, @Nullable FocusLevel level, Widget child) {\n        super(child);\n        this.state = state;\n        this.level = level;\n\n        this.inheritedKey = new InheritedKey(stateClass);\n    }\n\n    @Override\n    public Object inheritedKey() {\n        return this.inheritedKey;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        //noinspection unchecked\n        return ((FocusStateProvider<F>) newWidget).level != this.level;\n    }\n\n    // ---\n\n    public static <F extends Focusable.State<?>> Object keyOf(Class<F> stateClass) {\n        return new InheritedKey(stateClass);\n    }\n}\n\nrecord InheritedKey(Class<?> stateClass) {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/FocusTraversalDirection.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\npublic enum FocusTraversalDirection {\n    NEXT,\n    PREVIOUS,\n\n    UP,\n    DOWN,\n    LEFT,\n    RIGHT;\n\n    public FocusTraversalDirection opposite() {\n        return switch (this) {\n            case NEXT -> PREVIOUS;\n            case PREVIOUS -> NEXT;\n            case UP -> DOWN;\n            case DOWN -> UP;\n            case LEFT -> RIGHT;\n            case RIGHT -> LEFT;\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/Focusable.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\nimport java.util.stream.Stream;\n\npublic class Focusable extends StatefulWidget {\n\n    @Nullable private KeyDownCallback keyDownCallback;\n    @Nullable private KeyUpCallback keyUpCallback;\n    @Nullable private CharCallback charCallback;\n\n    @Nullable private FocusGainedCallback focusGainedCallback;\n    @Nullable private FocusLostCallback focusLostCallback;\n    @Nullable private FocusLevelChangedCallback focusLevelChangedCallback;\n\n    private boolean skipTraversal;\n    private boolean autoFocus;\n    @Nullable private Boolean clickFocus;\n\n    public final Widget child;\n\n    public Focusable(WidgetSetupCallback<Focusable> setupCallback, Widget child) {\n        setupCallback.setup(this);\n        this.child = child;\n    }\n\n    // ---\n\n    public static @Nullable State<?> maybeOf(BuildContext context) {\n        var provider = context.getAncestor(FocusStateProvider.class, FocusStateProvider.keyOf(State.class));\n        if (provider == null) return null;\n\n        return provider.state;\n    }\n\n    public static State<?> of(BuildContext context) {\n        var state = maybeOf(context);\n        Preconditions.checkNotNull(state, \"attempted to look up the closest Focusable without one present\");\n\n        return state;\n    }\n\n    public static @Nullable FocusLevel levelOf(BuildContext context) {\n        var provider = context.dependOnAncestor(FocusStateProvider.class, FocusStateProvider.keyOf(State.class));\n        return provider != null ? provider.level : null;\n    }\n\n    public static boolean isFocused(BuildContext context) {\n        return levelOf(context) != null;\n    }\n\n    public static boolean shouldShowHighlight(BuildContext context) {\n        return levelOf(context) == FocusLevel.HIGHLIGHT;\n    }\n\n    // ---\n\n    public Focusable keyDownCallback(@Nullable KeyDownCallback keyDownCallback) {\n        this.assertMutable();\n        this.keyDownCallback = keyDownCallback;\n        return this;\n    }\n\n    public @Nullable KeyDownCallback keyDownCallback() {\n        return this.keyDownCallback;\n    }\n\n    public Focusable keyUpCallback(@Nullable KeyUpCallback keyUpCallback) {\n        this.assertMutable();\n        this.keyUpCallback = keyUpCallback;\n        return this;\n    }\n\n    public @Nullable KeyUpCallback keyUpCallback() {\n        return this.keyUpCallback;\n    }\n\n    public Focusable charCallback(@Nullable CharCallback charCallback) {\n        this.assertMutable();\n        this.charCallback = charCallback;\n        return this;\n    }\n\n    public @Nullable CharCallback charCallback() {\n        return this.charCallback;\n    }\n\n    public Focusable focusGainedCallback(@Nullable FocusGainedCallback focusGainedCallback) {\n        this.assertMutable();\n        this.focusGainedCallback = focusGainedCallback;\n        return this;\n    }\n\n    public @Nullable FocusGainedCallback focusGainedCallback() {\n        return this.focusGainedCallback;\n    }\n\n    public Focusable focusLostCallback(@Nullable FocusLostCallback focusLostCallback) {\n        this.assertMutable();\n        this.focusLostCallback = focusLostCallback;\n        return this;\n    }\n\n    public @Nullable FocusLostCallback focusLostCallback() {\n        return this.focusLostCallback;\n    }\n\n    public Focusable focusLevelChangedCallback(@Nullable FocusLevelChangedCallback focusLevelChangedCallback) {\n        this.assertMutable();\n        this.focusLevelChangedCallback = focusLevelChangedCallback;\n        return this;\n    }\n\n    public @Nullable FocusLevelChangedCallback focusLevelChangedCallback() {\n        return this.focusLevelChangedCallback;\n    }\n\n    public Focusable skipTraversal(boolean skipTraversal) {\n        this.assertMutable();\n        this.skipTraversal = skipTraversal;\n        return this;\n    }\n\n    public boolean skipTraversal() {\n        return this.skipTraversal;\n    }\n\n    public Focusable autoFocus(boolean autoFocus) {\n        this.assertMutable();\n        this.autoFocus = autoFocus;\n        return this;\n    }\n\n    public boolean autoFocus() {\n        return this.autoFocus;\n    }\n\n    public Focusable clickFocus(@Nullable Boolean clickFocus) {\n        this.assertMutable();\n        this.clickFocus = clickFocus;\n        return this;\n    }\n\n    public @Nullable Boolean clickFocus() {\n        return this.clickFocus;\n    }\n\n    @Override\n    public WidgetState<?> createState() {\n        return new State<>();\n    }\n\n    @FunctionalInterface\n    public interface KeyDownCallback {\n        boolean onKeyDown(int keyCode, KeyModifiers modifiers);\n    }\n\n    @FunctionalInterface\n    public interface KeyUpCallback {\n        boolean onKeyUp(int keyCode, KeyModifiers modifiers);\n    }\n\n    @FunctionalInterface\n    public interface CharCallback {\n        boolean onChar(int charCode, KeyModifiers modifiers);\n    }\n\n    @FunctionalInterface\n    public interface FocusGainedCallback {\n        void onFocusGained();\n    }\n\n    @FunctionalInterface\n    public interface FocusLostCallback {\n        void onFocusLost();\n    }\n\n    @FunctionalInterface\n    public interface FocusLevelChangedCallback {\n        void onFocusLevelChanged(@Nullable FocusLevel level);\n    }\n\n    public static class State<F extends Focusable> extends WidgetState<F> {\n\n        private @Nullable State<?> parent;\n        private @Nullable FocusScope.State scope;\n        @Nullable FocusLevel level;\n\n        private int debugDepth;\n\n        public int debugDepth() {\n            return this.debugDepth;\n        }\n        \n        public State<?> primaryFocus() {\n            return this.scope != null ? this.scope.primaryFocus() : this;\n        }\n\n        public Stream<State<?>> ancestors() {\n            return Stream.iterate(\n                this.parent,\n                Objects::nonNull,\n                state -> state.parent\n            );\n        }\n\n        public void requestFocus() {\n            this.requestFocus(FocusLevel.HIGHLIGHT);\n        }\n\n        public void requestFocus(FocusLevel level) {\n            if (this.scope != null) {\n                this.scope.updateFocus(this, level);\n            }\n        }\n\n        public void unfocus() {\n            if (this.scope != null) {\n                this.scope.updateFocus(null, null);\n            }\n        }\n\n        public void traverseFocus(FocusTraversalDirection direction) {\n            if (this.scope != null) {\n                this.scope.traverseFocus(direction);\n            }\n        }\n\n        void onFocusChange(@Nullable FocusLevel newLevel) {\n            if (Owo.DEBUG) {\n                Preconditions.checkState(\n                    this.level != newLevel,\n                    String.format(\"_onFocusChange(%s) invoked on a state which is already at %s\", newLevel, newLevel)\n                );\n            }\n\n            if (this.widget().focusLevelChangedCallback() instanceof FocusLevelChangedCallback callback) {\n                callback.onFocusLevelChanged(newLevel);\n            }\n\n            if (this.level == null && newLevel != null) {\n                if (this.widget().focusGainedCallback() instanceof FocusGainedCallback callback) {\n                    callback.onFocusGained();\n                }\n            } else if (this.level != null && newLevel == null) {\n                if (this.widget().focusLostCallback() instanceof FocusLostCallback callback) {\n                    callback.onFocusLost();\n                }\n            }\n\n            this.setState(() -> {\n                this.level = newLevel;\n            });\n        }\n\n        void onClick() {\n            var shouldClickFocus = this.widget().clickFocus();\n            if (shouldClickFocus == Boolean.TRUE || this.context().getAncestor(FocusPolicy.class).clickFocus) {\n                this.requestFocus(FocusLevel.BASE);\n            }\n        }\n\n        boolean onKeyDown(int keyCode, KeyModifiers modifiers) {\n            if (Owo.DEBUG) {\n                Preconditions.checkState(\n                    this.level != null,\n                    \"onKeyDown invoked on a state which is not focused\"\n                );\n            }\n\n            return this.widget().keyDownCallback() instanceof KeyDownCallback callback && callback.onKeyDown(keyCode, modifiers);\n        }\n\n        boolean onKeyUp(int keyCode, KeyModifiers modifiers) {\n            if (Owo.DEBUG) {\n                Preconditions.checkState(\n                    this.level != null,\n                    \"onKeyUp invoked on a state which is not focused\"\n                );\n            }\n\n            return this.widget().keyUpCallback() instanceof KeyUpCallback callback && callback.onKeyUp(keyCode, modifiers);\n        }\n\n        boolean onChar(int charCode, KeyModifiers modifiers) {\n            if (Owo.DEBUG) {\n                Preconditions.checkState(\n                    this.level != null,\n                    \"onChar invoked on a state which is not focused\"\n                );\n            }\n\n            return this.widget().charCallback() instanceof CharCallback callback && callback.onChar(charCode, modifiers);\n        }\n\n        @Override\n        public void init() {\n            this.parent = Focusable.maybeOf(this.context());\n            this.scope = FocusScope.State.maybeOf(this.context());\n\n            this.debugDepth = this.parent != null ? this.parent.debugDepth + 1 : 0;\n\n            if (this.widget().autoFocus()) {\n                this.requestFocus();\n            }\n        }\n\n        @Override\n        public void dispose() {\n            if (this.scope != null) {\n                this.scope.onFocusableDisposed(this);\n            }\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new FocusClickArea(\n                this::onClick,\n                new FocusStateProvider<>(\n                    this,\n                    Focusable.State.class,\n                    this.level,\n                    this.widget().child\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/focus/RootFocusScope.java",
    "content": "package io.wispforest.owo.braid.widgets.focus;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventSource;\nimport io.wispforest.owo.braid.widgets.eventstream.StreamListenerState;\n\npublic class RootFocusScope extends StatefulWidget {\n\n    public final BraidEventSource<KeyDownEvent> onKeyDown;\n    public final BraidEventSource<KeyUpEvent> onKeyUp;\n    public final BraidEventSource<CharEvent> onChar;\n    public final Widget child;\n\n    public RootFocusScope(\n        BraidEventSource<KeyDownEvent> onKeyDown,\n        BraidEventSource<KeyUpEvent> onKeyUp,\n        BraidEventSource<CharEvent> onChar,\n        Widget child\n    ) {\n        this.onKeyDown = onKeyDown;\n        this.onKeyUp = onKeyUp;\n        this.onChar = onChar;\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<RootFocusScope> createState() {\n        return new State();\n    }\n\n    public static class State extends StreamListenerState<RootFocusScope> {\n\n        private FocusScope.State scope;\n\n        @Override\n        public void init() {\n            this.streamListen(widget -> widget.onKeyDown, event -> event.handled = this.scope.onKeyDown(event.keyCode, event.modifiers));\n            this.streamListen(widget -> widget.onKeyUp, event -> event.handled = this.scope.onKeyUp(event.keyCode, event.modifiers));\n            this.streamListen(widget -> widget.onChar, event -> event.handled = this.scope.onChar(event.charCode, event.modifiers));\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new FocusPolicy(\n                true,\n                new FocusScope(\n                    Widget.noSetup(),\n                    new Builder(scopeContext -> {\n                        if (this.scope == null) {\n                            this.scope = FocusScope.State.maybeOf(scopeContext);\n                            //noinspection DataFlowIssue\n                            this.scope.onFocusChange(FocusLevel.BASE);\n                        }\n\n                        return this.widget().child;\n                    })\n                )\n            );\n        }\n    }\n\n    // ---\n\n    private static class FocusEvent {\n        boolean handled = false;\n\n        public boolean handled() {\n            return this.handled;\n        }\n    }\n\n    public static final class KeyDownEvent extends FocusEvent {\n        private final int keyCode;\n        private final KeyModifiers modifiers;\n\n        public KeyDownEvent(int keyCode, KeyModifiers modifiers) {\n            this.keyCode = keyCode;\n            this.modifiers = modifiers;\n        }\n    }\n\n    public static final class KeyUpEvent extends FocusEvent {\n        private final int keyCode;\n        private final KeyModifiers modifiers;\n\n        public KeyUpEvent(int keyCode, KeyModifiers modifiers) {\n            this.keyCode = keyCode;\n            this.modifiers = modifiers;\n        }\n    }\n\n    public static final class CharEvent extends FocusEvent {\n        private final int charCode;\n        private final KeyModifiers modifiers;\n\n        public CharEvent(int charCode, KeyModifiers modifiers) {\n            this.charCode = charCode;\n            this.modifiers = modifiers;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/grid/Grid.java",
    "content": "package io.wispforest.owo.braid.widgets.grid;\n\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.framework.instance.InspectorProperty;\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.FontDescription;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.OptionalDouble;\nimport java.util.function.Function;\nimport java.util.function.ToDoubleFunction;\nimport java.util.stream.DoubleStream;\n\npublic class Grid extends MultiChildInstanceWidget {\n\n    public final LayoutAxis mainAxis;\n    public final int crossAxisCells;\n    public final CellFit cellFit;\n\n    public Grid(LayoutAxis mainAxis, int crossAxisCells, CellFit cellFit, List<? extends @Nullable Widget> children) {\n        super(children.stream().map(widget -> widget == null ? new Padding(Insets.none()) : widget).toList());\n        this.mainAxis = mainAxis;\n        this.cellFit = cellFit;\n        this.crossAxisCells = crossAxisCells;\n    }\n\n    public Grid(LayoutAxis mainAxis, int crossAxisCells, CellFit cellFit, @Nullable Widget... children) {\n        this(mainAxis, crossAxisCells, cellFit, Arrays.asList(children));\n    }\n\n    public Grid(LayoutAxis mainAxis, int crossAxisCells, CellFit cellFit, Function<Widget, Widget> cellWrapper, List<? extends @Nullable Widget> children) {\n        this(mainAxis, crossAxisCells, cellFit, children.stream().map(cellWrapper).toList());\n    }\n\n    public Grid(LayoutAxis mainAxis, int crossAxisCells, CellFit cellFit, Function<Widget, Widget> cellWrapper, @Nullable Widget... children) {\n        this(mainAxis, crossAxisCells, cellFit, cellWrapper, Arrays.asList(children));\n    }\n\n    @Override\n    public MultiChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends MultiChildWidgetInstance<Grid> {\n\n        private double[] debugCrossAxisSizes;\n        private double[] debugMainAxisSizes;\n\n        public Instance(Grid widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(Grid widget) {\n            if (this.widget.mainAxis == widget.mainAxis\n                && this.widget.crossAxisCells == widget.crossAxisCells\n                && this.widget.cellFit.equals(widget.cellFit)) {\n                return;\n            }\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var mainAxis = this.widget.mainAxis;\n            var crossAxis = mainAxis.opposite();\n\n            var crossAxisCells = this.widget.crossAxisCells;\n            var mainAxisCells = Mth.ceil(this.children.size() / (double) this.widget.crossAxisCells);\n\n            var mustMeasureCrossAxis = this.widget.cellFit.isTight() && !crossAxis.choose(constraints.hasTightWidth(), constraints.hasTightHeight());\n            var mustMeasureMainAxis = this.widget.cellFit.isTight() && !mainAxis.choose(constraints.hasTightWidth(), constraints.hasTightHeight());\n\n            var fixedCrossAxisCellSize = constraints.maxOnAxis(crossAxis) / crossAxisCells;\n            var fixedMainAxisCellSize = constraints.maxOnAxis(mainAxis) / mainAxisCells;\n\n            double @Nullable [] dynamicCrossAxisCellSizes = null;\n            if (mustMeasureCrossAxis) {\n                dynamicCrossAxisCellSizes = this.measureCrossAxis(fixedMainAxisCellSize);\n            }\n\n            double @Nullable [] dynamicMainAxisCellSizes = null;\n            if (mustMeasureMainAxis) {\n                dynamicMainAxisCellSizes = this.measureMainAxis(fixedCrossAxisCellSize);\n            }\n\n            for (int mainAxisIdx = 0; mainAxisIdx < mainAxisCells; mainAxisIdx++) {\n                var maxMainAxisChildSize = mustMeasureMainAxis\n                    ? dynamicMainAxisCellSizes[mainAxisIdx]\n                    : fixedMainAxisCellSize;\n\n                var firstChildIdx = mainAxisIdx * this.widget.crossAxisCells;\n                var lastChildIdx = Math.min(this.children.size(), firstChildIdx + this.widget.crossAxisCells) - 1;\n\n                for (var childIdx = firstChildIdx; childIdx <= lastChildIdx; childIdx++) {\n                    var child = this.children.get(childIdx);\n\n                    var maxCrossAxisChildSize = mustMeasureCrossAxis\n                        ? dynamicCrossAxisCellSizes[childIdx % this.widget.crossAxisCells]\n                        : fixedCrossAxisCellSize;\n\n                    var maxWidth = mainAxis == LayoutAxis.VERTICAL ? maxCrossAxisChildSize : maxMainAxisChildSize;\n                    var maxHeight = mainAxis == LayoutAxis.VERTICAL ? maxMainAxisChildSize : maxCrossAxisChildSize;\n\n                    var childConstraints = this.widget.cellFit.isTight()\n                        ? Constraints.tightOnAxis(maxWidth, maxHeight)\n                        : Constraints.loose(Size.of(maxWidth, maxHeight));\n\n                    child.layout(childConstraints);\n                }\n            }\n\n            var actualCrossAxisSizes = new double[crossAxisCells];\n            var actualMainAxisSizes = new double[mainAxisCells];\n\n            var minCrossAxisCellSize = constraints.minOnAxis(crossAxis) / crossAxisCells;\n            var minMainAxisCellSize = constraints.minOnAxis(mainAxis) / mainAxisCells;\n\n            for (var childIdx = 0; childIdx < this.children.size(); childIdx++) {\n                var child = this.children.get(childIdx);\n\n                var mainAxisCell = childIdx / crossAxisCells;\n                var crossAxisCell = childIdx % crossAxisCells;\n\n                actualCrossAxisSizes[crossAxisCell] = Math.max(minCrossAxisCellSize, Math.max(actualCrossAxisSizes[crossAxisCell], child.transform.getExtent(crossAxis)));\n                actualMainAxisSizes[mainAxisCell] = Math.max(minMainAxisCellSize, Math.max(actualMainAxisSizes[mainAxisCell], child.transform.getExtent(mainAxis)));\n            }\n\n            var alignment = this.widget.cellFit instanceof CellFit.Loose loose\n                ? loose.alignment\n                : Alignment.TOP_LEFT;\n\n            var mainAxisPos = 0d;\n            for (var mainAxisCell = 0; mainAxisCell < mainAxisCells; mainAxisCell++) {\n                var crossAxisPos = 0d;\n\n                for (var crossAxisCell = 0; crossAxisCell < crossAxisCells; crossAxisCell++) {\n                    var childIdx = mainAxisCell * crossAxisCells + crossAxisCell;\n                    if (childIdx >= this.children.size()) break;\n\n                    var child = this.children.get(childIdx);\n\n                    child.transform.setCoordinate(crossAxis, crossAxisPos + alignment.alignHorizontal(actualCrossAxisSizes[crossAxisCell], child.transform.getExtent(crossAxis)));\n                    child.transform.setCoordinate(mainAxis, mainAxisPos + alignment.alignVertical(actualMainAxisSizes[mainAxisCell], child.transform.getExtent(mainAxis)));\n\n                    crossAxisPos += actualCrossAxisSizes[crossAxisCell];\n                }\n\n                mainAxisPos += actualMainAxisSizes[mainAxisCell];\n            }\n\n            this.debugCrossAxisSizes = actualCrossAxisSizes;\n            this.debugMainAxisSizes = actualMainAxisSizes;\n\n            this.transform.setSize(\n                mainAxis == LayoutAxis.VERTICAL\n                    ? Size.of(DoubleStream.of(actualCrossAxisSizes).sum(), DoubleStream.of(actualMainAxisSizes).sum())\n                    : Size.of(DoubleStream.of(actualMainAxisSizes).sum(), DoubleStream.of(actualCrossAxisSizes).sum())\n            );\n        }\n\n        @Override\n        public List<InspectorProperty> debugListInspectorProperties() {\n            return List.of(\n                new InspectorProperty(\n                    Component.literal(\"Main Axis\"),\n                    Component.literal(this.widget.mainAxis.toString())\n                )\n            );\n        }\n\n        @Override\n        public boolean debugHasVisualizers() {\n            return true;\n        }\n\n        @Override\n        protected void debugDrawVisualizers(BraidGraphics graphics) {\n            var frameColor = Color.rgb(0xFFD65A);\n            graphics.drawRectOutline(\n                0, 0, (int) this.transform.width(), (int) this.transform.height(), frameColor.argb()\n            );\n\n            var verticalSizes = this.widget.mainAxis == LayoutAxis.VERTICAL\n                ? this.debugMainAxisSizes\n                : this.debugCrossAxisSizes;\n\n            var horizontalSizes = this.widget.mainAxis == LayoutAxis.VERTICAL\n                ? this.debugCrossAxisSizes\n                : this.debugMainAxisSizes;\n\n            var verticalPos = 0.0;\n            for (int i = 0; i < verticalSizes.length; i++) {\n                if (i > 0) {\n                    graphics.drawDashedLine(\n                        RenderPipelines.GUI,\n                        0, verticalPos, this.transform.width(), verticalPos,\n                        1, 2, frameColor\n                    );\n                }\n\n                graphics.drawText(\n                    Component.literal(verticalSizes[i] + \"px\").withStyle(style -> style.withFont(new FontDescription.Resource(Minecraft.UNIFORM_FONT))),\n                    0, (float) verticalPos, 1f, Color.WHITE.argb(),\n                    OwoUIGraphics.TextAnchor.TOP_RIGHT\n                );\n\n                verticalPos += verticalSizes[i];\n            }\n\n            var horizontalPos = 0.0;\n            for (int i = 0; i < horizontalSizes.length; i++) {\n                if (i > 0) {\n                    graphics.drawDashedLine(\n                        RenderPipelines.GUI,\n                        horizontalPos, 0, horizontalPos, this.transform.height(),\n                        1, 2, frameColor\n                    );\n                }\n\n                graphics.drawText(\n                    Component.literal(horizontalSizes[i] + \"px\").withStyle(style -> style.withFont(new FontDescription.Resource(Minecraft.UNIFORM_FONT))),\n                    (float) horizontalPos, 0, 1f, Color.WHITE.argb(),\n                    OwoUIGraphics.TextAnchor.BOTTOM_LEFT\n                );\n\n                horizontalPos += horizontalSizes[i];\n            }\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.widget.mainAxis == LayoutAxis.VERTICAL\n                ? DoubleStream.of(this.measureCrossAxis(height)).sum()\n                : DoubleStream.of(this.measureMainAxis(height)).sum();\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.widget.mainAxis == LayoutAxis.VERTICAL\n                ? DoubleStream.of(this.measureMainAxis(width)).sum()\n                : DoubleStream.of(this.measureCrossAxis(width)).sum();\n        }\n\n        protected double[] measureCrossAxis(double mainAxisCellSize) {\n            var crossAxisCells = this.widget.crossAxisCells;\n            var measureFunction = this.widget.mainAxis.opposite().<ToDoubleFunction<WidgetInstance<?>>>chooseCompute(\n                () -> child -> child.getIntrinsicWidth(mainAxisCellSize),\n                () -> child -> child.getIntrinsicHeight(mainAxisCellSize)\n            );\n\n            var intrinsics = this.children.stream().mapToDouble(measureFunction).toArray();\n\n            var sizes = new double[crossAxisCells];\n            for (var cell = 0; cell < crossAxisCells; cell++) {\n                var cellSize = 0d;\n\n                for (var childIdx = cell; childIdx < this.children.size(); childIdx += crossAxisCells) {\n                    cellSize = Math.max(cellSize, intrinsics[childIdx]);\n                }\n\n                sizes[cell] = cellSize;\n            }\n\n            return sizes;\n        }\n\n        protected double[] measureMainAxis(double crossAxisCellSize) {\n            var crossAxisCells = this.widget.crossAxisCells;\n            var mainAxisCells = Mth.ceil(this.children.size() / (double) this.widget.crossAxisCells);\n\n            var measureFunction = this.widget.mainAxis.<ToDoubleFunction<WidgetInstance<?>>>chooseCompute(\n                () -> child -> child.getIntrinsicWidth(crossAxisCellSize),\n                () -> child -> child.getIntrinsicHeight(crossAxisCellSize)\n            );\n\n            var intrinsics = this.children.stream().mapToDouble(measureFunction).toArray();\n\n            var sizes = new double[mainAxisCells];\n            for (var cell = 0; cell < mainAxisCells; cell++) {\n                var cellSize = 0d;\n\n                var firstChild = cell * crossAxisCells;\n                var lastChild = firstChild + (crossAxisCells - 1);\n\n                for (var childIdx = firstChild; childIdx <= lastChild; childIdx++) {\n                    if (childIdx >= this.children.size()) break;\n                    cellSize = Math.max(cellSize, intrinsics[childIdx]);\n                }\n\n                sizes[cell] = cellSize;\n            }\n\n            return sizes;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.computeHighestBaselineOffset();\n        }\n    }\n\n    public static sealed abstract class CellFit {\n\n        public abstract boolean isTight();\n\n        public static CellFit loose() {\n            return loose(Alignment.CENTER);\n        }\n\n        public static CellFit loose(Alignment alignment) {\n            return new Loose(alignment);\n        }\n\n        public static CellFit tight() {\n            return Tight.INSTANCE;\n        }\n\n        public static final class Tight extends CellFit {\n            public static final Tight INSTANCE = new Tight();\n\n            @Override\n            public boolean isTight() {\n                return true;\n            }\n        }\n\n        public static final class Loose extends CellFit {\n            public final Alignment alignment;\n            public Loose(Alignment alignment) {this.alignment = alignment;}\n\n            @Override\n            public boolean isTight() {\n                return false;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/BraidInspector.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.BraidWindow;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventSource;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventStream;\nimport net.minecraft.util.Unit;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\npublic class BraidInspector {\n\n    public final AppState subject;\n    public WidgetProxy rootProxy;\n    public WidgetInstance<?> rootInstance;\n\n    private final BraidEventStream<Unit> refreshEvents = new BraidEventStream<>();\n    private final BraidEventStream<Unit> pickEvents = new BraidEventStream<>();\n    private final BraidEventStream<RevealInstanceEvent> revealEvents = new BraidEventStream<>();\n\n    private boolean active = false;\n    @Nullable AppState currentApp;\n    @Nullable BraidWindow currentWindow;\n\n    public BraidInspector(AppState subject) {\n        this.subject = subject;\n    }\n\n    public BraidEventSource<Unit> onPick() {\n        return this.pickEvents.source();\n    }\n\n    public void pick() {\n        this.pickEvents.sink().onEvent(Unit.INSTANCE);\n    }\n\n    public BraidEventSource<Unit> onRefresh() {\n        return this.refreshEvents.source();\n    }\n\n    public BraidEventSource<RevealInstanceEvent> onReveal() {\n        return this.revealEvents.source();\n    }\n\n    public void activate() {\n        if (this.rootProxy == null || this.rootInstance == null) {\n            throw new IllegalStateException(\"cannot activate the braid inspector before the root proxy and instance have been set\");\n        }\n\n        if (this.currentApp != null) {\n            GLFW.glfwShowWindow(this.currentWindow.handle);\n            return;\n        }\n\n        if (this.active) return;\n        this.active = true;\n\n        var result = BraidWindow.open(\n            \"braid inspector\",\n            900,\n            550,\n            new InspectorWidget(this.rootProxy, this.rootInstance, this)\n        );\n\n        GLFW.glfwSetWindowAttrib(result.window().handle, GLFW.GLFW_FLOATING, GLFW.GLFW_TRUE);\n\n        this.currentApp = result.state();\n        this.currentWindow = result.window();\n\n        this.currentApp.onTerminate(() -> {\n            this.currentApp = null;\n            this.currentWindow = null;\n            this.active = false;\n        });\n    }\n\n    public void revealInstance(WidgetInstance<?> instance) {\n        if (!this.active) return;\n        this.revealEvents.sink().onEvent(new RevealInstanceEvent(instance));\n    }\n\n    public void refresh() {\n        this.refreshEvents.sink().onEvent(Unit.INSTANCE);\n    }\n\n    public void close() {\n        if (this.currentApp == null) return;\n        this.currentApp.scheduleShutdown();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/CollapsibleEntry.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.collapsible.LazyCollapsible;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventSource;\nimport io.wispforest.owo.braid.widgets.eventstream.StreamListenerState;\nimport io.wispforest.owo.braid.widgets.intents.Intent;\nimport io.wispforest.owo.braid.widgets.intents.Interactable;\nimport io.wispforest.owo.braid.widgets.intents.ShortcutTrigger;\nimport net.minecraft.util.Unit;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class CollapsibleEntry extends StatefulWidget {\n\n    public final BraidEventSource<Unit> onExpand;\n    public final boolean startCollapsed;\n    public final Widget title;\n    public final Widget content;\n\n    public CollapsibleEntry(BraidEventSource<Unit> onExpand, boolean startCollapsed, Widget title, Widget content) {\n        this.onExpand = onExpand;\n        this.startCollapsed = startCollapsed;\n        this.title = title;\n        this.content = content;\n    }\n\n    @Override\n    public WidgetState<CollapsibleEntry> createState() {\n        return new State();\n    }\n\n    public static class State extends StreamListenerState<CollapsibleEntry> {\n\n        private boolean collapsed;\n\n        private void expand(Unit unit) {\n            this.setState(() -> {\n                this.collapsed = false;\n            });\n        }\n\n        @Override\n        public void init() {\n            this.streamListen(widget -> widget.onExpand, this::expand);\n            this.collapsed = this.widget().startCollapsed;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Interactable(\n                SHORTCUTS,\n                widget -> widget\n                    .addCallbackAction(SetCollapsedIntent.class, (actionCtx, intent) -> {\n                        this.setState(() -> this.collapsed = intent.collapsed());\n                    }),\n                new LazyCollapsible(\n                    true,\n                    this.collapsed,\n                    nowCollapsed -> this.setState(() -> this.collapsed = nowCollapsed),\n                    this.widget().title,\n                    this.widget().content\n                )\n            );\n        }\n    }\n\n    // ---\n\n    public static final Map<List<ShortcutTrigger>, Intent> SHORTCUTS = Map.of(\n        List.of(ShortcutTrigger.LEFT), new SetCollapsedIntent(true),\n        List.of(ShortcutTrigger.RIGHT), new SetCollapsedIntent(false)\n    );\n}\n\nrecord SetCollapsedIntent(boolean collapsed) implements Intent {}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/InspectorState.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.braid.widgets.sharedstate.ShareableState;\nimport org.jetbrains.annotations.Nullable;\n\npublic class InspectorState extends ShareableState {\n    public @Nullable Object selectedElement;\n    public @Nullable RevealInstanceEvent lastRevealEvent;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/InspectorWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.Button;\nimport io.wispforest.owo.braid.widgets.eventstream.StreamListenerState;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.label.DefaultLabelStyle;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.scroll.DefaultScrollAnimationSettings;\nimport io.wispforest.owo.braid.widgets.scroll.FlatScrollbar;\nimport io.wispforest.owo.braid.widgets.scroll.ScrollAnimationSettings;\nimport io.wispforest.owo.braid.widgets.scroll.ScrollableWithBars;\nimport io.wispforest.owo.braid.widgets.sharedstate.SharedState;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.FontDescription;\nimport net.minecraft.network.chat.Style;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.List;\n\npublic class InspectorWidget extends StatefulWidget {\n\n    private final WidgetProxy rootProxy;\n    private final WidgetInstance<?> rootInstance;\n    private final BraidInspector inspector;\n\n    public InspectorWidget(WidgetProxy rootProxy, WidgetInstance<?> rootInstance, BraidInspector inspector) {\n        this.rootProxy = rootProxy;\n        this.rootInstance = rootInstance;\n        this.inspector = inspector;\n    }\n\n    @Override\n    public WidgetState<InspectorWidget> createState() {\n        return new State();\n    }\n\n    public static class State extends StreamListenerState<InspectorWidget> {\n\n        public InspectorState inspectorState;\n        public boolean alwaysOnTop = true;\n\n        @Override\n        public void init() {\n            this.streamListen(\n                widget -> widget.inspector.onRefresh(),\n                unit -> setState(() -> {})\n            );\n\n            this.streamListen(\n                widget -> widget.inspector.onReveal(),\n                event -> this.inspectorState.setState(() -> {\n                    this.inspectorState.selectedElement = event.instance;\n                    this.inspectorState.lastRevealEvent = event;\n                })\n            );\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new DefaultScrollAnimationSettings(\n                ScrollAnimationSettings.DEFAULT,\n                new SharedState<>(\n                    InspectorState::new,\n                    new Builder(stateContext -> {\n                        this.inspectorState = SharedState.getWithoutDependency(stateContext, InspectorState.class);\n\n                        return new Box(\n                            Color.rgb(0x1d2026),\n                            new DefaultLabelStyle(\n                                new LabelStyle(null, null, Style.EMPTY.withFont(new FontDescription.Resource(Minecraft.UNIFORM_FONT)), null),\n                                new Row(\n                                    new Flexible(\n                                        new Stack(\n                                            new ScrollableWithBars(\n                                                null,\n                                                null,\n                                                null,\n                                                3,\n                                                (axis, controller) -> new FlatScrollbar(axis, controller, Color.rgb(0xabb0bf), Color.rgb(0xabb0bf)),\n                                                new Align(\n                                                    Alignment.TOP_LEFT,\n                                                    new InstanceTreeView(this.widget().inspector.onReveal(), this.widget().rootInstance)\n                                                )\n                                            ),\n                                            new Align(\n                                                Alignment.BOTTOM_RIGHT,\n                                                new Padding(\n                                                    Insets.all(5),\n                                                    new Row(\n                                                        new Padding(Insets.horizontal(1)),\n                                                        List.of(\n                                                            new Sized(\n                                                                20,\n                                                                20,\n                                                                new Tooltip(\n                                                                    Component.literal(this.alwaysOnTop ? \"window behavior:\\nalways on top\" : \"window behavior:\\nnormal\"),\n                                                                    new Button(\n                                                                        () -> this.setState(() -> {\n                                                                            this.alwaysOnTop = !this.alwaysOnTop;\n                                                                            GLFW.glfwSetWindowAttrib(this.widget().inspector.currentWindow.handle, GLFW.GLFW_FLOATING, this.alwaysOnTop ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE);\n                                                                        }),\n                                                                        new SpriteWidget(\n                                                                            this.alwaysOnTop\n                                                                                ? Owo.id(\"braid_inspector_always_on_top\")\n                                                                                : Owo.id(\"braid_inspector_not_always_on_top\")\n                                                                        )\n                                                                    )\n                                                                )\n                                                            ),\n                                                            new Sized(\n                                                                20,\n                                                                20,\n                                                                new Tooltip(\n                                                                    Component.literal(\"reassemble app\"),\n                                                                    new Button(\n                                                                        () -> this.widget().inspector.subject.rebuildRoot(),\n                                                                        new SpriteWidget(Owo.id(\"braid_inspector_reassemble\"))\n                                                                    )\n                                                                )\n                                                            ),\n                                                            new Sized(\n                                                                20,\n                                                                20,\n                                                                new Tooltip(\n                                                                    Component.literal(\"pick widget\"),\n                                                                    new Button(\n                                                                        () -> this.widget().inspector.pick(),\n                                                                        new SpriteWidget(Owo.id(\"braid_inspector_pick\"))\n                                                                    )\n                                                                )\n                                                            )\n                                                        )\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    ),\n                                    new InstanceDetails()\n                                )\n                            )\n                        );\n                    })\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/InstanceDetails.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.checkbox.Checkbox;\nimport io.wispforest.owo.braid.widgets.checkbox.CheckboxStyle;\nimport io.wispforest.owo.braid.widgets.flex.*;\nimport io.wispforest.owo.braid.widgets.grid.Grid;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.sharedstate.SharedState;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.chat.Component;\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2f;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Pattern;\nimport java.util.stream.Stream;\n\npublic class InstanceDetails extends StatefulWidget {\n\n    @Override\n    public WidgetState<?> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<InstanceDetails> {\n        @Override\n        public Widget build(BuildContext context) {\n            var selected = SharedState.select(context, InspectorState.class, state -> state.selectedElement);\n\n            List<Widget> children;\n            if (selected instanceof WidgetInstance<?> instance) {\n                var instanceClassName = instance.getClass().getName();\n                var matcher = INSTANCE_NAME_PATTERN.matcher(instanceClassName);\n                instanceClassName = matcher.matches() ? matcher.group(1) : instanceClassName;\n\n                children = new ArrayList<>();\n                children.add(new Grid(\n                    LayoutAxis.VERTICAL,\n                    2,\n                    Grid.CellFit.tight(),\n                    colorRows(\n                        Color.rgb(0x111319),\n                        2,\n                        gatherProperties(instance).stream().<Widget>map(Label::new).toList()\n                    )\n                ));\n\n                if (instance.debugHasVisualizers()) {\n                    children.add(new Padding(\n                        Insets.of(5, 0, 5, 0),\n                        new Row(\n                            MainAxisAlignment.START,\n                            CrossAxisAlignment.CENTER,\n                            new Checkbox(\n                                CheckboxStyle.BRAID,\n                                instance.debugDrawVisualizers,\n                                nowChecked -> setState(() -> {\n                                    instance.debugDrawVisualizers = nowChecked;\n                                })\n                            ),\n                            new Padding(\n                                Insets.left(5),\n                                Label.literal(\"draw visualizers\")\n                            )\n                        )\n                    ));\n                }\n\n                children.addAll(List.of(\n                    new Flexible(new Padding(Insets.none())),\n                    new Label(Component.literal(instanceClassName))\n                ));\n            } else {\n                children = List.of(new Flexible(\n                    new Center(\n                        new Label(Component.literal(\"no instance selected\"))\n                    )\n                ));\n            }\n\n            return new Row(\n                new Sized(1, null, new Box(Color.WHITE)),\n                new Sized(\n                    150,\n                    null,\n                    new Column(\n                        Stream.concat(\n                            Stream.of(new Padding(Insets.bottom(3), new Label(Component.literal(\"Instance Details\")))),\n                            children.stream()\n                        ).toList()\n                    )\n                )\n            );\n        }\n\n        private static List<Component> gatherProperties(WidgetInstance<?> instance) {\n            var instanceTransform = instance.hasParent() ? instance.parent().computeGlobalTransform().invert() : new Matrix3x2f();\n            var absPos = instanceTransform.transformPosition((float) instance.transform.x(), (float) instance.transform.y(), new Vector2f());\n\n            var properties = new ArrayList<>(List.<Component>of(\n                    Component.literal(\"Rel. Position\").withStyle(ChatFormatting.BOLD),\n                    Component.literal(rounded(instance.transform.x()) + \", \" + rounded(instance.transform.y())),\n                    Component.literal(\"Abs. Position\").withStyle(ChatFormatting.BOLD),\n                    Component.literal(rounded(absPos.x()) + \", \" + rounded(absPos.y())),\n                    Component.literal(\"Width\").withStyle(ChatFormatting.BOLD),\n                    Component.literal(instance.transform.width() + \"px\"),\n                    Component.literal(\"Height\").withStyle(ChatFormatting.BOLD),\n                    Component.literal(instance.transform.height() + \"px\"),\n                    Component.literal(\"Widget\").withStyle(ChatFormatting.BOLD),\n                    Component.literal(instance.widget().getClass().getSimpleName())\n            ));\n\n            for (var property : instance.debugListInspectorProperties()) {\n                properties.add(property.name().copy().withStyle(ChatFormatting.BOLD));\n                properties.add(property.value());\n            }\n\n            return properties;\n        }\n\n        private static List<Widget> colorRows(Color alternateColor, int crossAxisCells, List<Widget> cells) {\n            var result = new ArrayList<Widget>();\n\n            var mainAxisIdx = 0;\n            var crossAxisIdx = 0;\n            for (var widget : cells) {\n                widget = new Padding(Insets.vertical(2), widget);\n\n                if (mainAxisIdx % 2 == 0) {\n                    result.add(widget);\n                } else {\n                    result.add(new Box(alternateColor, false, widget));\n                }\n\n                if (++crossAxisIdx == crossAxisCells) {\n                    crossAxisIdx = 0;\n                    mainAxisIdx++;\n                }\n            }\n\n            return result;\n        }\n\n        private static String rounded(double value) {\n            return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).toPlainString();\n        }\n\n        // ---\n\n        private static final Pattern INSTANCE_NAME_PATTERN = Pattern.compile(\"^.*?([A-Za-z]\\\\w+\\\\$?Instance)$\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/InstancePicker.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport com.google.common.collect.Streams;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.HitTestState;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventSource;\nimport io.wispforest.owo.braid.widgets.eventstream.StreamListenerState;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport net.minecraft.util.Unit;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.ArrayList;\nimport java.util.Comparator;\n\npublic class InstancePicker extends StatefulWidget {\n\n    public final BraidEventSource<Unit> activateEvents;\n    public final PickCallback pickCallback;\n    public final Widget child;\n\n    public InstancePicker(BraidEventSource<Unit> activateEvents, PickCallback pickCallback, Widget child) {\n        this.activateEvents = activateEvents;\n        this.pickCallback = pickCallback;\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<InstancePicker> createState() {\n        return new State();\n    }\n\n    public static class State extends StreamListenerState<InstancePicker> {\n\n        private BuildContext childContext;\n        private @Nullable WidgetInstance<?> pickedInstance;\n\n        private boolean picking = false;\n\n        @Override\n        public void init() {\n            this.streamListen(widget -> widget.activateEvents, unit -> {\n                this.setState(() -> this.picking = true);\n            });\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var children = new ArrayList<Widget>();\n\n            children.add(new StackBase(\n                new Builder(childContext -> {\n                    this.childContext = childContext;\n                    return this.widget().child;\n                })\n            ));\n\n            if (this.picking) {\n                children.add(new MouseArea(\n                    widget -> widget\n                        .moveCallback((toX, toY) -> {\n                            var hitTest = new HitTestState();\n                            this.childContext.instance().hitTest(toX, toY, hitTest);\n\n                            if (this.pickedInstance != null) this.pickedInstance.debugHighlighted = false;\n\n                            var pickHit = Streams.stream(hitTest.occludedTrace())\n                                .min(Comparator.comparingDouble(value -> value.instance().transform.width() * value.instance().transform.height()))\n                                .orElse(null);\n\n                            this.pickedInstance = pickHit != null ? pickHit.instance() : null;\n\n                            if (this.pickedInstance != null) this.pickedInstance.debugHighlighted = true;\n                        })\n                        .clickCallback((x, y, button, modifiers) -> {\n                            if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) {\n                                if (this.pickedInstance != null) {\n                                    this.pickedInstance.debugHighlighted = false;\n                                    this.widget().pickCallback.onPick(this.pickedInstance);\n                                }\n\n                                this.setState(() -> this.picking = false);\n                            }\n\n                            return true;\n                        }),\n                    new Padding(Insets.none())\n                ));\n            }\n\n            return new Stack(children);\n        }\n    }\n\n    @FunctionalInterface\n    public interface PickCallback {\n        void onPick(WidgetInstance<?> pickedInstance);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/InstanceTitle.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.sharedstate.SharedState;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.chat.Component;\n\nimport java.util.regex.Pattern;\n\npublic class InstanceTitle extends StatefulWidget {\n    public final WidgetInstance<?> instance;\n    public InstanceTitle(WidgetInstance<?> instance) {\n        this.instance = instance;\n    }\n\n    @Override\n    public WidgetState<InstanceTitle> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<InstanceTitle> {\n\n        public boolean hovered = false;\n\n        @Override\n        public Widget build(BuildContext context) {\n            var selected = SharedState.select(context, InspectorState.class, state -> state.selectedElement) == this.widget().instance;\n\n            var instanceName = this.widget().instance.getClass().getName();\n            var matcher = INSTANCE_NAME_PATTERN.matcher(instanceName);\n\n            if (matcher.matches()) {\n                instanceName = matcher.group(1).replaceAll(\"\\\\$\", \".\");\n            }\n\n            var title = new Panel(\n                selected ? Owo.id(\"braid_inspector_selected\") : null,\n                new Padding(\n                    Insets.all(2),\n                    new Row(\n                        MainAxisAlignment.START,\n                        CrossAxisAlignment.CENTER,\n                        new Label(Component.literal(instanceName).withStyle(style -> style.withBold(this.hovered))),\n                        new Visibility(\n                            this.widget().instance.isRelayoutBoundary() && this.widget().instance.debugParentHasDependency(),\n                            new Padding(\n                                Insets.left(2),\n                                new Tooltip(\n                                    Component.literal(\"Relayout Boundary\\n\").append(Component.literal(\"with parent dependency\").withStyle(ChatFormatting.GRAY)),\n                                    new SpriteWidget(Owo.id(\"braid_inspector_relayout_boundary_with_dependency\"))\n                                )\n                            )\n                        ),\n                        new Visibility(\n                            this.widget().instance.isRelayoutBoundary() && !this.widget().instance.debugParentHasDependency(),\n                            new Padding(\n                                Insets.left(2),\n                                new Tooltip(\n                                    Component.literal(\"Relayout Boundary\"),\n                                    new SpriteWidget(Owo.id(\"braid_inspector_relayout_boundary\"))\n                                )\n                            )\n                        ),\n                        new Visibility(\n                            (this.widget().instance.flags & WidgetInstance.FLAG_HIT_TEST_BOUNDARY) != 0,\n                            new Padding(\n                                Insets.left(2),\n                                new Tooltip(\n                                    Component.literal(\"Hit Test Boundary\"),\n                                    new SpriteWidget(Owo.id(\"braid_inspector_hit_test_boundary\"))\n                                )\n                            )\n                        )\n                    )\n                )\n            );\n\n            return new Focusable(\n                widget -> widget\n                    .focusGainedCallback(() -> SharedState.set(context, InspectorState.class, state -> state.selectedElement = this.widget().instance)),\n                new MouseArea(\n                    widget -> widget\n                        .enterCallback(() -> this.setState(() -> {\n                            this.widget().instance.debugHighlighted = true;\n                            this.hovered = true;\n                        }))\n                        .exitCallback(() -> this.setState(() -> {\n                            this.widget().instance.debugHighlighted = false;\n                            this.hovered = false;\n                        }))\n                        .cursorStyle(CursorStyle.CROSSHAIR),\n                    title\n                )\n            );\n        }\n\n        // ---\n\n        private static final Pattern INSTANCE_NAME_PATTERN = Pattern.compile(\"^.*?([A-Za-z]\\\\w+)\\\\$?Instance$\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/InstanceTreeView.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.animation.Easing;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.animated.AnimatedBox;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventSource;\nimport io.wispforest.owo.braid.widgets.eventstream.BraidEventStream;\nimport io.wispforest.owo.braid.widgets.eventstream.StreamListenerState;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.scroll.Scrollable;\nimport io.wispforest.owo.braid.widgets.sharedstate.SharedState;\nimport net.minecraft.util.Unit;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\n\npublic class InstanceTreeView extends StatefulWidget {\n\n    public final BraidEventSource<RevealInstanceEvent> revealEvents;\n    public final WidgetInstance<?> viewInstance;\n\n    public InstanceTreeView(BraidEventSource<RevealInstanceEvent> revealEvents, WidgetInstance<?> viewInstance) {\n        this.revealEvents = revealEvents;\n        this.viewInstance = viewInstance;\n    }\n\n    @Override\n    public WidgetState<InstanceTreeView> createState() {\n        return new State();\n    }\n\n    public static class State extends StreamListenerState<InstanceTreeView> {\n\n        public final BraidEventStream<Unit> expandEvents = new BraidEventStream<>();\n\n        public boolean builtOnce = false;\n        public boolean highlight = false;\n\n        private void reveal() {\n            this.schedulePostLayoutCallback(() -> {\n                Scrollable.reveal(this.context(), Insets.all(20));\n            });\n        }\n\n        @Override\n        public void init() {\n            this.streamListen(\n                widget -> widget.revealEvents,\n                event -> {\n                    if (event.instance == this.widget().viewInstance) {\n                        this.reveal();\n                    }\n\n                    if (event.fullPath.contains(this.widget().viewInstance)) {\n                        this.expandEvents.sink().onEvent(Unit.INSTANCE);\n                    }\n                }\n            );\n        }\n\n        @Override\n        public void didUpdateWidget(InstanceTreeView oldWidget) {\n            if (oldWidget.viewInstance != this.widget().viewInstance) {\n                this.setState(() -> {\n                    this.highlight = true;\n                });\n            }\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var title = new InstanceTitle(this.widget().viewInstance);\n\n            var children = new ArrayList<WidgetInstance<?>>();\n            this.widget().viewInstance.visitChildren(children::add);\n\n            if (this.highlight) {\n                this.schedulePostLayoutCallback(() -> this.setState(() -> this.highlight = false));\n            }\n\n            var startCollapsed = true;\n            if (!this.builtOnce) {\n                this.builtOnce = true;\n\n                var lastRevealEvent = SharedState.getWithoutDependency(context, InspectorState.class).lastRevealEvent;\n                if (lastRevealEvent != null && lastRevealEvent.instance == this.widget().viewInstance) {\n                    this.reveal();\n                }\n\n                startCollapsed = lastRevealEvent == null || !lastRevealEvent.fullPath.contains(this.widget().viewInstance);\n            }\n\n            return new AnimatedBox(\n                this.highlight ? Duration.ZERO : Duration.ofMillis(1250),\n                Easing.IN_OUT_SINE,\n                this.highlight ? Color.hsv((this.widget().viewInstance.depth() % 15) / 15d, .75, 1, .5) : new Color(0),\n                true,\n                !children.isEmpty()\n                    ?\n                    new CollapsibleEntry(\n                        this.expandEvents.source(),\n                        startCollapsed,\n                        title,\n                        new Column(\n                            children.stream()\n                                .map(child -> new InstanceTreeView(this.widget().revealEvents, child))\n                                .toList()\n                        )\n                    )\n                    :\n                        new Row(\n                            MainAxisAlignment.START,\n                            CrossAxisAlignment.CENTER,\n                            new Sized(\n                                12,\n                                12,\n                                new SpriteWidget(Owo.id(\"braid_inspector_leaf\"))\n                            ),\n                            title\n                        )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/inspector/RevealInstanceEvent.java",
    "content": "package io.wispforest.owo.braid.widgets.inspector;\n\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\n\nimport java.util.Set;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class RevealInstanceEvent {\n    public final WidgetInstance<?> instance;\n    public final Set<WidgetInstance<?>> fullPath;\n\n    public RevealInstanceEvent(WidgetInstance<?> instance) {\n        this.instance = instance;\n        this.fullPath = Stream.concat(\n            this.instance.ancestors().stream(),\n            Stream.of(this.instance)\n        ).collect(Collectors.toSet());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/Action.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\n\npublic abstract class Action<I extends Intent> {\n\n    public boolean isActive(BuildContext context, I intent) {\n        return true;\n    }\n\n    public abstract void invoke(BuildContext context, I intent);\n\n    public static <I extends Intent> Action<I> callback(Callback<I> callback) {\n        return new CallbackAction<>(callback);\n    }\n\n    public static class CallbackAction<I extends Intent> extends Action<I> {\n\n        public Callback<I> callback;\n\n        public CallbackAction(Callback<I> callback) {\n            this.callback = callback;\n        }\n\n        @Override\n        public void invoke(BuildContext context, I intent) {\n            this.callback.invoke(context, intent);\n        }\n    }\n\n    @FunctionalInterface\n    public interface Callback<I extends Intent> {\n        void invoke(BuildContext actionCtx, I intent);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/Actions.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class Actions extends StatefulWidget {\n\n    private boolean focusable = true;\n    private boolean autoFocus = false;\n    private boolean skipTraversal = false;\n\n    private final Map<Class<? extends Intent>, Action<?>> actions;\n    public final Widget child;\n\n    public Actions(@Nullable WidgetSetupCallback<Actions> setup, Widget child) {\n        this.actions = new HashMap<>();\n        this.child = child;\n        if (setup != null) setup.setup(this);\n    }\n\n    public Actions focusable(boolean focusable) {\n        this.assertMutable();\n        this.focusable = focusable;\n        return this;\n    }\n\n    public boolean focusable() {\n        return this.focusable;\n    }\n\n    public Actions autoFocus(boolean autoFocus) {\n        this.assertMutable();\n        this.autoFocus = autoFocus;\n        return this;\n    }\n\n    public boolean autoFocus() {\n        return this.autoFocus;\n    }\n\n    public Actions skipTraversal(boolean skipTraversal) {\n        this.assertMutable();\n        this.skipTraversal = skipTraversal;\n        return this;\n    }\n\n    public boolean skipTraversal() {\n        return this.skipTraversal;\n    }\n\n    public Actions actions(Map<Class<? extends Intent>, Action<?>> actions) {\n        this.assertMutable();\n        this.actions.putAll(actions);\n        return this;\n    }\n\n    public <I extends Intent> Actions addAction(Class<I> intentType, Action<I> action) {\n        if (this.actions.containsKey(intentType)) {\n            throw new IllegalArgumentException(\"Duplicate intent type: \" + intentType);\n        }\n\n        this.actions.put(intentType, action);\n        return this;\n    }\n\n    public <I extends Intent> Actions addCallbackAction(Class<I> intentType, Action.Callback<I> callback) {\n        this.addAction(intentType, Action.callback(callback));\n        return this;\n    }\n\n    public Map<Class<? extends Intent>, Action<?>> actions() {\n        return this.actions;\n    }\n\n    @Override\n    public WidgetState<Actions> createState() {\n        return new State();\n    }\n\n    // ---\n\n    public static boolean invoke(BuildContext context, Intent intent) {\n        var action = actionForIntent(context, intent);\n        if (action != null) {\n            action.invoke(context, intent);\n            return true;\n        }\n\n        return false;\n    }\n\n    @SuppressWarnings({\"unchecked\"})\n    public static @Nullable <I extends Intent> Action<I> actionForIntent(BuildContext context, I intent) {\n        var intents = context.getAncestor(ActionsProvider.class);\n        while (intents != null) {\n            var action = intents.state.widget().actions.get(intent.getClass());\n            if (action != null && ((Action<I>) action).isActive(context, intent)) {\n                break;\n            }\n\n            intents = intents.state.context().getAncestor(ActionsProvider.class);\n        }\n\n        return intents != null\n            ? (Action<I>) intents.state.widget().actions.get(intent.getClass())\n            : null;\n    }\n\n    // ---\n\n    public static class State extends WidgetState<Actions> {\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            return new ActionsProvider(\n                this,\n                widget.focusable\n                    ? new Focusable(focusable -> focusable.autoFocus(widget.autoFocus).skipTraversal(this.widget().skipTraversal), widget.child)\n                    : widget.child\n            );\n        }\n    }\n}\n\nclass ActionsProvider extends InheritedWidget {\n\n    public final Actions.State state;\n\n    public ActionsProvider(Actions.State state, Widget child) {\n        super(child);\n        this.state = state;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/AdjustIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\npublic record AdjustIntent(Direction direction) implements Intent {\n    public enum Direction {\n        INCREMENT, DECREMENT;\n\n        public int offset() {\n            return switch (this) {\n                case INCREMENT -> 1;\n                case DECREMENT -> -1;\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/Intent.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\npublic interface Intent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/Interactable.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class Interactable extends StatefulWidget {\n\n    private @Nullable MouseArea.EnterCallback enterCallback;\n    private @Nullable MouseArea.ExitCallback exitCallback;\n    private @Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier;\n\n    private @Nullable Focusable.FocusGainedCallback focusGainedCallback;\n    private @Nullable Focusable.FocusLostCallback focusLostCallback;\n    private boolean skipTraversal = false;\n    private boolean autoFocus = false;\n\n    public final Map<List<ShortcutTrigger>, Intent> shortcuts;\n\n    private final Map<Class<? extends Intent>, Action<?>> actions = new HashMap<>();\n\n    public final Widget child;\n\n    public Interactable(\n        @Nullable Map<List<ShortcutTrigger>, Intent> shortcuts,\n        WidgetSetupCallback<Interactable> setup,\n        Widget child\n    ) {\n        this.shortcuts = shortcuts != null ? shortcuts : Map.of();\n        this.child = child;\n        setup.setup(this);\n    }\n\n    public static Widget primary(@Nullable Runnable onClick, @Nullable WidgetSetupCallback<Interactable> setup, Widget child) {\n        return new Interactable(\n            CLICK_SHORTCUT,\n            widget -> {\n                if (onClick != null) {\n                    widget\n                        .addAction(PrimaryActionIntent.class, Action.callback((context, intent) -> onClick.run()))\n                        .cursorStyle(CursorStyle.HAND);\n                }\n\n                if (setup != null) {\n                    setup.setup(widget);\n                }\n            },\n            child\n        );\n    }\n\n    public static Widget primary(Runnable onClick, Widget child) {\n        return primary(onClick, null, child);\n    }\n\n    // ---\n\n    public Interactable enterCallback(@Nullable MouseArea.EnterCallback enterCallback) {\n        this.assertMutable();\n        this.enterCallback = enterCallback;\n        return this;\n    }\n\n    public @Nullable MouseArea.EnterCallback enterCallback() {\n        return this.enterCallback;\n    }\n\n    public Interactable exitCallback(@Nullable MouseArea.ExitCallback exitCallback) {\n        this.assertMutable();\n        this.exitCallback = exitCallback;\n        return this;\n    }\n\n    public @Nullable MouseArea.ExitCallback exitCallback() {\n        return this.exitCallback;\n    }\n\n    public Interactable cursorStyleSupplier(@Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier) {\n        this.assertMutable();\n        this.cursorStyleSupplier = cursorStyleSupplier;\n        return this;\n    }\n\n    public Interactable cursorStyle(@Nullable CursorStyle style) {\n        return this.cursorStyleSupplier((x, y) -> style);\n    }\n\n    public @Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier() {\n        return this.cursorStyleSupplier;\n    }\n\n    public Interactable focusGainedCallback(@Nullable Focusable.FocusGainedCallback focusGainedCallback) {\n        this.assertMutable();\n        this.focusGainedCallback = focusGainedCallback;\n        return this;\n    }\n\n    public @Nullable Focusable.FocusGainedCallback focusGainedCallback() {\n        return this.focusGainedCallback;\n    }\n\n    public Interactable focusLostCallback(@Nullable Focusable.FocusLostCallback focusLostCallback) {\n        this.assertMutable();\n        this.focusLostCallback = focusLostCallback;\n        return this;\n    }\n\n    public @Nullable Focusable.FocusLostCallback focusLostCallback() {\n        return this.focusLostCallback;\n    }\n\n    public Interactable skipTraversal(boolean skipTraversal) {\n        this.assertMutable();\n        this.skipTraversal = skipTraversal;\n        return this;\n    }\n\n    public boolean skipTraversal() {\n        return this.skipTraversal;\n    }\n\n    public Interactable autoFocus(boolean autoFocus) {\n        this.assertMutable();\n        this.autoFocus = autoFocus;\n        return this;\n    }\n\n    public boolean autoFocus() {\n        return this.autoFocus;\n    }\n\n    public Interactable actions(Map<Class<? extends Intent>, Action<?>> actions) {\n        this.assertMutable();\n        this.actions.putAll(actions);\n        return this;\n    }\n\n    public <I extends Intent> Interactable addAction(Class<I> intentType, Action<I> action) {\n        if (this.actions.containsKey(intentType)) {\n            throw new IllegalArgumentException(\"Duplicate intent type: \" + intentType);\n        }\n\n        this.actions.put(intentType, action);\n        return this;\n    }\n\n    public <I extends Intent> Interactable addCallbackAction(Class<I> intentType, Action.Callback<I> callback) {\n        this.addAction(intentType, Action.callback(callback));\n        return this;\n    }\n\n    public Map<Class<? extends Intent>, Action<?>> actions() {\n        return this.actions;\n    }\n\n    @Override\n    public WidgetState<Interactable> createState() {\n        return new State();\n    }\n\n    // ---\n\n    private static final Map<List<ShortcutTrigger>, Intent> CLICK_SHORTCUT = Map.of(\n        List.of(ShortcutTrigger.LEFT_CLICK), PrimaryActionIntent.INSTANCE\n    );\n\n    // ---\n\n    public static class State extends WidgetState<Interactable> {\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            return new Actions(\n                actions -> actions\n                    .focusable(false)\n                    .actions(this.widget().actions),\n                new Shortcuts(\n                    widget.shortcuts,\n                    shortcuts -> shortcuts\n                        .enterCallback(this.widget().enterCallback)\n                        .exitCallback(this.widget().exitCallback)\n                        .cursorStyleSupplier(this.widget().cursorStyleSupplier)\n                        .focusGainedCallback(this.widget().focusGainedCallback)\n                        .focusLostCallback(this.widget().focusLostCallback)\n                        .skipTraversal(this.widget().skipTraversal)\n                        .autoFocus(this.widget().autoFocus),\n                    widget.child\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/PrimaryActionIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\npublic class PrimaryActionIntent implements Intent {\n    private PrimaryActionIntent() {}\n    public static final PrimaryActionIntent INSTANCE = new PrimaryActionIntent();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/SecondaryActionIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\npublic class SecondaryActionIntent implements Intent {\n    private SecondaryActionIntent() {}\n    public static final SecondaryActionIntent INSTANCE = new SecondaryActionIntent();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/ShortcutDecoder.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport com.google.common.collect.Iterables;\nimport io.wispforest.owo.braid.core.BraidUtils;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport net.minecraft.util.Tuple;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\n\npublic class ShortcutDecoder extends StatefulWidget {\n    private @Nullable MouseArea.EnterCallback enterCallback;\n    private @Nullable MouseArea.ExitCallback exitCallback;\n    private @Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier;\n\n    private @Nullable Focusable.FocusGainedCallback focusGainedCallback;\n    private @Nullable Focusable.FocusLostCallback focusLostCallback;\n    private boolean skipTraversal = false;\n    private boolean autoFocus = false;\n\n    private final Map<List<ShortcutTrigger>, Listener> shortcuts = new LinkedHashMap<>();\n\n    private final Widget child;\n\n    public ShortcutDecoder(\n        WidgetSetupCallback<ShortcutDecoder> setupCallback,\n        Widget child\n    ) {\n        this.child = child;\n        setupCallback.setup(this);\n    }\n\n    public ShortcutDecoder enterCallback(@Nullable MouseArea.EnterCallback enterCallback) {\n        this.assertMutable();\n        this.enterCallback = enterCallback;\n        return this;\n    }\n\n    public @Nullable MouseArea.EnterCallback enterCallback() {\n        return this.enterCallback;\n    }\n\n    public ShortcutDecoder exitCallback(@Nullable MouseArea.ExitCallback exitCallback) {\n        this.assertMutable();\n        this.exitCallback = exitCallback;\n        return this;\n    }\n\n    public @Nullable MouseArea.ExitCallback exitCallback() {\n        return this.exitCallback;\n    }\n\n    public ShortcutDecoder cursorStyleSupplier(@Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier) {\n        this.assertMutable();\n        this.cursorStyleSupplier = cursorStyleSupplier;\n        return this;\n    }\n\n    public ShortcutDecoder cursorStyle(@Nullable CursorStyle style) {\n        return this.cursorStyleSupplier((x, y) -> style);\n    }\n\n    public @Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier() {\n        return this.cursorStyleSupplier;\n    }\n\n    public ShortcutDecoder focusGainedCallback(@Nullable Focusable.FocusGainedCallback focusGainedCallback) {\n        this.assertMutable();\n        this.focusGainedCallback = focusGainedCallback;\n        return this;\n    }\n\n    public @Nullable Focusable.FocusGainedCallback focusGainedCallback() {\n        return this.focusGainedCallback;\n    }\n\n    public ShortcutDecoder focusLostCallback(@Nullable Focusable.FocusLostCallback focusLostCallback) {\n        this.assertMutable();\n        this.focusLostCallback = focusLostCallback;\n        return this;\n    }\n\n    public @Nullable Focusable.FocusLostCallback focusLostCallback() {\n        return this.focusLostCallback;\n    }\n\n    public ShortcutDecoder skipTraversal(boolean skipTraversal) {\n        this.assertMutable();\n        this.skipTraversal = skipTraversal;\n        return this;\n    }\n\n    public boolean skipTraversal() {\n        return this.skipTraversal;\n    }\n\n    public ShortcutDecoder autoFocus(boolean autoFocus) {\n        this.assertMutable();\n        this.autoFocus = autoFocus;\n        return this;\n    }\n\n    public boolean autoFocus() {\n        return this.autoFocus;\n    }\n\n    public ShortcutDecoder shortcuts(Map<List<ShortcutTrigger>, Listener> shortcuts) {\n        this.assertMutable();\n        this.shortcuts.putAll(shortcuts);\n        return this;\n    }\n\n    public ShortcutDecoder addShortcut(List<ShortcutTrigger> triggers, Listener action) {\n        this.assertMutable();\n        this.shortcuts.put(triggers, action);\n        return this;\n    }\n\n    public ShortcutDecoder addShortcut(ShortcutTrigger trigger, Listener action) {\n        return this.addShortcut(List.of(trigger), action);\n    }\n\n    public Map<List<ShortcutTrigger>, Listener> shortcuts() {\n        return this.shortcuts;\n    }\n\n    @Override\n    public WidgetState<ShortcutDecoder> createState() {\n        return new State();\n    }\n\n    @FunctionalInterface\n    public interface Listener {\n        boolean trigger(TriggerType type);\n    }\n\n    public static class State extends WidgetState<ShortcutDecoder> {\n        private List<ShortcutSequence> sequences = new ArrayList<>();\n\n        private final List<ShortcutSequence> queuedSequences = new ArrayList<>();\n        @Nullable private Long callbackId;\n\n        @Override\n        public void init() {\n            this.buildSequences();\n        }\n\n        @Override\n        public void didUpdateWidget(ShortcutDecoder oldWidget) {\n            this.buildSequences();\n        }\n\n        private void buildSequences() {\n            this.sequences = widget().shortcuts.entrySet().stream().map(emongus -> new ShortcutSequence(emongus.getKey(), emongus.getValue())).toList();\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new MouseArea(\n                widget -> widget\n                    .enterCallback(this.widget().enterCallback())\n                    .exitCallback(this.widget().exitCallback())\n                    .cursorStyleSupplier(this.widget().cursorStyleSupplier())\n                    .clickCallback((x, y, button, modifiers) -> stepShortcuts(trigger -> trigger.isTriggeredByMouseButton(button, modifiers)\n                        ? ShortcutTriggerResult.ACTIVATED\n                        : ShortcutTriggerResult.NOT_ACTIVATED, TriggerType.MOUSE)),\n                new Focusable(\n                    widget -> widget\n                        .focusGainedCallback(this.widget().focusGainedCallback())\n                        .focusLostCallback(this.widget().focusLostCallback())\n                        .skipTraversal(this.widget().skipTraversal())\n                        .autoFocus(this.widget().autoFocus())\n                        .keyDownCallback((keyCode, modifiers) -> stepShortcuts(trigger -> {\n                            if (trigger.isTriggeredByKeyCode(keyCode, modifiers)) return ShortcutTriggerResult.ACTIVATED;\n                            return KeyModifiers.isModifier(keyCode) ? ShortcutTriggerResult.IGNORED : ShortcutTriggerResult.NOT_ACTIVATED;\n                        }, TriggerType.KEY)),\n                    this.widget().child\n                )\n            );\n        }\n\n        private boolean stepShortcuts(Function<ShortcutTrigger, ShortcutTriggerResult> test, TriggerType trigger) {\n            // in case we currently have a dispatch queued, we\n            // must cancel it *now* to avoid prematurely triggering\n            // a dispatch before the user is done entering triggers\n            if (this.callbackId != null) {\n                this.cancelDelayedCallback(this.callbackId);\n                this.callbackId = null;\n            }\n\n            // now, begin by stepping all sequences with current input and keeping\n            // only the ones which didn't ignore it. this can lead to a few outcomes\n            // for each sequence. to break it down:\n            // - singular sequences:\n            //   these can always step and, if so, will immediately complete\n            // - non-singular sequences:\n            //   whether these can step depends on their current state:\n            //   - non-negative trigger index:\n            //     if triggered, will step and potentially complete\n            //     if not triggered, will not step and poison the trigger index\n            //   - negative (poisoned) trigger index:\n            //     will not step\n            var steppedSequences = sequences.stream()\n                .map(sequence -> new Tuple<>(sequence, sequence.step(test)))\n                .filter(pair -> pair.getB() != ShortcutSequenceStep.IGNORE)\n                .toList();\n\n            // next, get the sequence to treat as completed on this iteration - if any\n            // - if multiple sequences completed, pick the first one\n            // - always prioritize non-singular sequences over singular sequences.\n            //   this is important, since the current trigger could both finish\n            //   a non-singular sequence (user intent) and immediately complete a\n            //   singular one (this would be an artifact)\n            var completed = BraidUtils.fold(\n                Iterables.filter(steppedSequences, pair -> pair.getB() == ShortcutSequenceStep.COMPLETE),\n                (Tuple<ShortcutSequence, ShortcutSequenceStep>) null,\n                (acc, element) -> {\n                    if (acc == null) return element;\n                    if (!element.getA().isSingular && acc.getA().isSingular) return element;\n                    return acc;\n                }\n            );\n            //(I personally think this should've used stream.reduce but glisco said it was \"not ideal\" so here we are) -chyz\n\n            // if we have successfully resolved all ambiguity, that is,\n            // every remaining (non-poisoned) sequence stepped to completion,\n            // dispatch immediately\n            if (steppedSequences.stream().allMatch(pair -> pair.getB() == ShortcutSequenceStep.COMPLETE) && completed != null) {\n                return this.dispatch(completed.getA(), completed.getA().isSingular, trigger);\n            } else {\n                // otherwise, queue up the completed sequence (if any)\n                // and queue dispatch after the maximum possible input delay\n\n                if (completed != null) {\n                    // if the sequence we just complete is non-singular, clear\n                    // the queue - this is important, since otherwise we could duplicate\n                    // the respective events\n                    if (!completed.getA().isSingular) {\n                        this.queuedSequences.clear();\n                    }\n\n                    this.queuedSequences.add(completed.getA());\n                    completed.getA().nextTriggerIndex = 0;\n                }\n\n                this.callbackId = this.scheduleDelayedCallback(MAX_INPUT_DELAY, () -> this.dispatch(null, true, trigger));\n\n                return !steppedSequences.isEmpty();\n            }\n\n        }\n\n        private boolean dispatch(@Nullable ShortcutDecoder.State.ShortcutSequence completedSequence, boolean runQueued, TriggerType trigger) {\n            if (runQueued) {\n                for (var sequence : this.queuedSequences) {\n                    sequence.callback.trigger(trigger);\n                }\n            }\n\n            var success = false;\n            if (completedSequence != null) {\n                success = completedSequence.callback.trigger(trigger);\n            }\n\n            this.queuedSequences.clear();\n            for (var sequence : sequences) {\n                sequence.nextTriggerIndex = 0;\n            }\n\n            return success;\n        }\n\n        public static final Duration MAX_INPUT_DELAY = Duration.ofMillis(250);\n\n        private enum ShortcutTriggerResult {\n            /// the trigger was not activated by this input.\n            /// non-singular sequences should poison\n            NOT_ACTIVATED,\n            /// the trigger was activated by this input.\n            /// sequences should step\n            ACTIVATED,\n            /// the trigger entirely ignored this input.\n            /// non-singular sequences should not poison\n            /// and sequences should not step\n            IGNORED\n        }\n\n        private enum ShortcutSequenceStep {\n            IGNORE,\n            ADVANCE,\n            COMPLETE\n        }\n\n        private static class ShortcutSequence {\n            public final List<ShortcutTrigger> triggers;\n            public final Listener callback;\n\n            /// whether this sequence is singular, i.e. it only has\n            /// a single trigger and can be completed at any time\n            public final boolean isSingular;\n\n            public int nextTriggerIndex = 0;\n\n            public ShortcutSequence(List<ShortcutTrigger> triggers, Listener callback) {\n                this.triggers = triggers;\n                this.callback = callback;\n                this.isSingular = triggers.size() == 1;\n            }\n\n            /// step this sequence\n            /// - if the sequence ignored the input, is poisoned or is completed, return [ShortcutSequenceStep#IGNORE]\n            /// - if the sequence activated its final trigger, return [ShortcutSequenceStep#COMPLETE]\n            /// - if the sequence activated an intermediate trigger, return [ShortcutSequenceStep#ADVANCE]\n            public ShortcutSequenceStep step(Function<ShortcutTrigger, ShortcutTriggerResult> test) {\n                if (this.nextTriggerIndex < 0 || this.nextTriggerIndex >= this.triggers.size()) return ShortcutSequenceStep.IGNORE;\n\n                var result = test.apply(this.triggers.get(this.nextTriggerIndex));\n                if (result == ShortcutTriggerResult.ACTIVATED) {\n                    this.nextTriggerIndex++;\n                    return this.nextTriggerIndex == this.triggers.size() ? ShortcutSequenceStep.COMPLETE : ShortcutSequenceStep.ADVANCE;\n                } else if (!this.isSingular && result == ShortcutTriggerResult.NOT_ACTIVATED) {\n                    // only poison non-singular sequences. this is important, because\n                    // otherwise we could incorrectly swallow a singular sequence completed\n                    // just after the first trigger of a non-singular sequence\n                    this.nextTriggerIndex = -1;\n                }\n                return ShortcutSequenceStep.IGNORE;\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/ShortcutTrigger.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport static org.lwjgl.glfw.GLFW.*;\n\npublic record ShortcutTrigger(Set<Trigger> triggers) {\n\n    public static final ShortcutTrigger LEFT_CLICK = new ShortcutTrigger(Trigger.ofMouse(GLFW_MOUSE_BUTTON_LEFT));\n    public static final ShortcutTrigger RIGHT_CLICK = new ShortcutTrigger(Trigger.ofMouse(GLFW_MOUSE_BUTTON_RIGHT));\n\n    public static final ShortcutTrigger UP = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_UP)\n    );\n\n    public static final ShortcutTrigger DOWN = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_DOWN)\n    );\n\n    public static final ShortcutTrigger RIGHT = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_RIGHT)\n    );\n\n    public static final ShortcutTrigger LEFT = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_LEFT)\n    );\n\n    public static final ShortcutTrigger PAGE_UP = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_PAGE_UP)\n    );\n\n    public static final ShortcutTrigger PAGE_DOWN = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_PAGE_DOWN)\n    );\n\n    public static final ShortcutTrigger HOME = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_HOME)\n    );\n\n    public static final ShortcutTrigger END = new ShortcutTrigger(\n        Trigger.ofKey(GLFW_KEY_END)\n    );\n\n    public static ShortcutTrigger of(ShortcutTrigger... triggers) {\n        return new ShortcutTrigger(Arrays.stream(triggers).flatMap(actionTrigger -> actionTrigger.triggers.stream()).collect(Collectors.toSet()));\n    }\n\n    public static ShortcutTrigger of(ShortcutTrigger actionTrigger, Trigger... triggers) {\n        var combinedTriggers = new HashSet<>(actionTrigger.triggers);\n        combinedTriggers.addAll(Arrays.asList(triggers));\n        return new ShortcutTrigger(combinedTriggers);\n    }\n\n    public ShortcutTrigger(Collection<Trigger> triggers) {\n        this(Set.copyOf(triggers));\n    }\n\n    public ShortcutTrigger(Trigger... triggers) {\n        this(Set.of(triggers));\n    }\n\n    public ShortcutTrigger withModifiers(@Nullable KeyModifiers modifiers) {\n        var triggers = new HashSet<Trigger>();\n        for (var trigger : this.triggers) {\n            triggers.add(trigger.withModifiers(modifiers));\n        }\n\n        return new ShortcutTrigger(triggers);\n    }\n\n    public boolean isTriggeredByMouseButton(int button, KeyModifiers modifiers) {\n        return this.triggers.stream().anyMatch(trigger -> trigger instanceof Trigger.Mouse mouseTrigger && mouseTrigger.isTriggered(button, modifiers));\n    }\n\n    public boolean isTriggeredByKeyCode(int keyCode, KeyModifiers modifiers) {\n        return this.triggers.stream().anyMatch(trigger -> trigger instanceof Trigger.Key keyTrigger && keyTrigger.isTriggered(keyCode, modifiers));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/Shortcuts.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class Shortcuts extends StatefulWidget {\n\n    private @Nullable MouseArea.EnterCallback enterCallback;\n    private @Nullable MouseArea.ExitCallback exitCallback;\n    private @Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier;\n\n    private @Nullable Focusable.FocusGainedCallback focusGainedCallback;\n    private @Nullable Focusable.FocusLostCallback focusLostCallback;\n    private boolean skipTraversal = false;\n    private boolean autoFocus = false;\n\n    public final Map<List<ShortcutTrigger>, Intent> shortcuts;\n    public final Widget child;\n\n    public Shortcuts(Map<List<ShortcutTrigger>, Intent> shortcuts, @Nullable WidgetSetupCallback<Shortcuts> setup, Widget child) {\n        this.shortcuts = shortcuts;\n        this.child = child;\n        if (setup != null) setup.setup(this);\n    }\n\n    public Shortcuts enterCallback(@Nullable MouseArea.EnterCallback enterCallback) {\n        this.assertMutable();\n        this.enterCallback = enterCallback;\n        return this;\n    }\n\n    public @Nullable MouseArea.EnterCallback enterCallback() {\n        return this.enterCallback;\n    }\n\n    public Shortcuts exitCallback(@Nullable MouseArea.ExitCallback exitCallback) {\n        this.assertMutable();\n        this.exitCallback = exitCallback;\n        return this;\n    }\n\n    public @Nullable MouseArea.ExitCallback exitCallback() {\n        return this.exitCallback;\n    }\n\n    public Shortcuts cursorStyleSupplier(@Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier) {\n        this.assertMutable();\n        this.cursorStyleSupplier = cursorStyleSupplier;\n        return this;\n    }\n\n    public Shortcuts cursorStyle(@Nullable CursorStyle style) {\n        return this.cursorStyleSupplier((x, y) -> style);\n    }\n\n    public @Nullable MouseArea.CursorStyleSupplier cursorStyleSupplier() {\n        return this.cursorStyleSupplier;\n    }\n\n    public Shortcuts focusGainedCallback(@Nullable Focusable.FocusGainedCallback focusGainedCallback) {\n        this.assertMutable();\n        this.focusGainedCallback = focusGainedCallback;\n        return this;\n    }\n\n    public @Nullable Focusable.FocusGainedCallback focusGainedCallback() {\n        return this.focusGainedCallback;\n    }\n\n    public Shortcuts focusLostCallback(@Nullable Focusable.FocusLostCallback focusLostCallback) {\n        this.assertMutable();\n        this.focusLostCallback = focusLostCallback;\n        return this;\n    }\n\n    public @Nullable Focusable.FocusLostCallback focusLostCallback() {\n        return this.focusLostCallback;\n    }\n\n    public Shortcuts skipTraversal(boolean skipTraversal) {\n        this.assertMutable();\n        this.skipTraversal = skipTraversal;\n        return this;\n    }\n\n    public boolean skipTraversal() {\n        return this.skipTraversal;\n    }\n\n    public Shortcuts autoFocus(boolean autoFocus) {\n        this.assertMutable();\n        this.autoFocus = autoFocus;\n        return this;\n    }\n\n    public boolean autoFocus() {\n        return this.autoFocus;\n    }\n\n    @Override\n    public WidgetState<Shortcuts> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<Shortcuts> {\n\n        private Map<List<ShortcutTrigger>, ShortcutDecoder.Listener> listeners;\n\n        @Override\n        public void init() {\n            this.buildListeners();\n        }\n\n        @Override\n        public void didUpdateWidget(Shortcuts oldWidget) {\n            this.buildListeners();\n        }\n\n        private void buildListeners() {\n            this.listeners = new HashMap<>();\n            this.widget().shortcuts.forEach((triggers, intent) -> {\n                this.listeners.put(triggers, type -> {\n                    var sourceContext = switch (type) {\n                        case KEY -> Focusable.of(this.context()).primaryFocus().context();\n                        case MOUSE -> this.context();\n                    };\n\n                    return Actions.invoke(sourceContext, intent);\n                });\n            });\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new ShortcutDecoder(\n                widget -> widget\n                    .shortcuts(this.listeners)\n                    .enterCallback(this.widget().enterCallback)\n                    .exitCallback(this.widget().exitCallback)\n                    .cursorStyleSupplier(this.widget().cursorStyleSupplier)\n                    .focusGainedCallback(this.widget().focusGainedCallback)\n                    .focusLostCallback(this.widget().focusLostCallback)\n                    .skipTraversal(this.widget().skipTraversal)\n                    .autoFocus(this.widget().autoFocus),\n                this.widget().child\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/TraverseFocusAction.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\n\npublic class TraverseFocusAction extends Action<TraverseFocusIntent> {\n    @Override\n    public void invoke(BuildContext context, TraverseFocusIntent intent) {\n        Focusable.of(context).traverseFocus(intent.direction());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/TraverseFocusIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.widgets.focus.FocusTraversalDirection;\n\npublic record TraverseFocusIntent(FocusTraversalDirection direction) implements Intent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/Trigger.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\n\npublic sealed interface Trigger {\n\n    boolean isTriggered(int button, @Nullable KeyModifiers modifiers);\n\n    Trigger withModifiers(@Nullable KeyModifiers modifiers);\n\n    static Trigger.Key ofKey(int keyCode, @Nullable KeyModifiers modifiers) {\n        return new Key(keyCode, modifiers);\n    }\n\n    static Trigger.Key ofKey(int keyCode) {\n        return new Key(keyCode);\n    }\n\n    static Trigger.Mouse ofMouse(int button, @Nullable KeyModifiers modifiers) {\n        return new Mouse(button, modifiers);\n    }\n\n    static Trigger.Mouse ofMouse(int button) {\n        return new Mouse(button);\n    }\n\n    record Key(int keyCode, @Nullable KeyModifiers modifiers) implements Trigger {\n\n        public Key(int keyCode) {\n            this(keyCode, KeyModifiers.NONE);\n        }\n\n        @Override\n        public boolean isTriggered(int button, KeyModifiers modifiers) {\n            return this.keyCode == button && (this.modifiers == null || this.modifiers.equals(modifiers));\n        }\n\n        @Override\n        public Trigger withModifiers(@Nullable KeyModifiers modifiers) {\n            return !Objects.equals(this.modifiers, modifiers)\n                ? new Key(this.keyCode, modifiers)\n                : this;\n        }\n    }\n\n    record Mouse(int button, @Nullable KeyModifiers modifiers) implements Trigger {\n\n        public Mouse(int button) {\n            this(button, KeyModifiers.NONE);\n        }\n\n        @Override\n        public boolean isTriggered(int button, KeyModifiers modifiers) {\n            return this.button == button && (this.modifiers == null || this.modifiers.equals(modifiers));\n        }\n\n        @Override\n        public Trigger withModifiers(@Nullable KeyModifiers modifiers) {\n            return !Objects.equals(this.modifiers, modifiers)\n                ? new Mouse(this.button, modifiers)\n                : this;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/intents/TriggerType.java",
    "content": "package io.wispforest.owo.braid.widgets.intents;\n\npublic enum TriggerType {\n    KEY, MOUSE\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/label/DefaultLabelStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.label;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport org.jetbrains.annotations.Nullable;\n\npublic class DefaultLabelStyle extends InheritedWidget {\n\n    public final LabelStyle style;\n\n    public DefaultLabelStyle(LabelStyle style, Widget child) {\n        super(child);\n        this.style = style;\n    }\n\n    public static Widget merge(LabelStyle style, Widget child) {\n        return new Builder(context -> {\n            var contextStyle = DefaultLabelStyle.maybeOf(context);\n            return new DefaultLabelStyle(contextStyle != null ? style.overriding(contextStyle) : style, child);\n        });\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return !this.style.equals(((DefaultLabelStyle) newWidget).style);\n    }\n\n    public static @Nullable LabelStyle maybeOf(BuildContext context) {\n        var widget = context.dependOnAncestor(DefaultLabelStyle.class);\n        if (widget != null) {\n            return widget.style;\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/label/Label.java",
    "content": "package io.wispforest.owo.braid.widgets.label;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Clip;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Label extends StatelessWidget {\n\n    public final @Nullable LabelStyle style;\n    public final boolean softWrap;\n    public final Overflow overflow;\n    public final Component text;\n\n    public Label(@Nullable LabelStyle style, boolean softWrap, Overflow overflow, Component text) {\n        this.style = style;\n        this.softWrap = softWrap;\n        this.overflow = overflow;\n        this.text = text;\n    }\n\n    public Label(@Nullable LabelStyle style, boolean softWrap, Component text) {\n        this(style, softWrap, Overflow.CLIP, text);\n    }\n\n    public Label(boolean softWrap, Component text) {\n        this(null, softWrap, text);\n    }\n\n    public Label(Overflow overflow, Component text) {\n        this(null, true, overflow, text);\n    }\n\n    public Label(Component text) {\n        this(true, text);\n    }\n\n    public static Label literal(String text) {\n        return new Label(Component.literal(text));\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        var effectiveStyle = this.style != null ? this.style : LabelStyle.EMPTY;\n        if (DefaultLabelStyle.maybeOf(context) instanceof LabelStyle contextStyle) {\n            effectiveStyle = effectiveStyle.overriding(contextStyle);\n        }\n\n        Widget result = new RawLabel(\n            effectiveStyle.fillDefaults(),\n            this.softWrap,\n            this.overflow == Overflow.ELLIPSIS,\n            this.text\n        );\n\n        if (this.overflow == Overflow.CLIP) {\n            result = new Clip(result);\n        }\n\n        return result;\n    }\n\n    public enum Overflow {\n        SHOW, CLIP, ELLIPSIS\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/label/LabelStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.label;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport net.minecraft.network.chat.Style;\nimport org.jetbrains.annotations.Nullable;\n\npublic record LabelStyle(@Nullable Alignment textAlignment, @Nullable Color baseColor, @Nullable Style textStyle, @Nullable Boolean shadow) {\n    public static final LabelStyle EMPTY = new LabelStyle(null, null, null, null);\n    public static final LabelStyle SHADOW = new LabelStyle(null, null, null, true);\n\n    public LabelStyle overriding(LabelStyle other) {\n        return new LabelStyle(\n            this.textAlignment != null ? this.textAlignment : other.textAlignment,\n            this.baseColor != null ? this.baseColor : other.baseColor,\n            this.textStyle != null ? this.textStyle : other.textStyle,\n            this.shadow != null ? this.shadow : other.shadow\n        );\n    }\n\n    public LabelStyle fillDefaults() {\n        return new LabelStyle(\n            this.textAlignment != null ? this.textAlignment : Alignment.CENTER,\n            this.baseColor != null ? this.baseColor : Color.WHITE,\n            this.textStyle != null ? this.textStyle : Style.EMPTY,\n            this.shadow != null ? this.shadow : false\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/label/RawLabel.java",
    "content": "package io.wispforest.owo.braid.widgets.label;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.MouseListener;\nimport io.wispforest.owo.braid.framework.instance.TooltipProvider;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport io.wispforest.owo.mixin.braid.ClickableStyleFinderAccessor;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport it.unimi.dsi.fastutil.doubles.DoubleArrayList;\nimport it.unimi.dsi.fastutil.doubles.DoubleList;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.ActiveTextCollector;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.locale.Language;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.FormattedText;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2f;\n\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.OptionalDouble;\nimport java.util.function.Function;\n\npublic class RawLabel extends LeafInstanceWidget {\n\n    public final LabelStyle style;\n    public final boolean softWrap;\n    public final boolean ellipsize;\n    public final Component text;\n\n    public RawLabel(LabelStyle style, boolean softWrap, boolean ellipsize, Component text) {\n        this.style = style;\n        this.softWrap = softWrap;\n        this.ellipsize = ellipsize;\n        this.text = text;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends LeafWidgetInstance<RawLabel> implements TooltipProvider, MouseListener {\n\n        private List<FormattedCharSequence> renderText = List.of();\n        private DoubleList renderTextWidths = new DoubleArrayList();\n        private int renderTextHeight = 0;\n\n        protected Function<Style, Boolean> textClickHandler = style -> {\n            return style != null && OwoUIGraphics.utilityScreen().handleTextClick(style, Minecraft.getInstance().screen);\n        };\n\n        public Instance(RawLabel widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(RawLabel widget) {\n            if (Objects.equals(this.widget.style, widget.style)\n                && this.widget.softWrap == widget.softWrap\n                && this.widget.ellipsize == widget.ellipsize\n                && Objects.equals(this.widget.text, widget.text)) {\n                return;\n            }\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        protected List<FormattedCharSequence> wrapText(Font font, int maxWidth, double maxHeight) {\n            var styledText = this.widget.text.copy().withStyle(textStyle -> textStyle.applyTo(this.widget.style.textStyle()));\n            var wrappedLines = font.getSplitter().splitLines(styledText, this.widget.softWrap ? maxWidth : Integer.MAX_VALUE, Style.EMPTY);\n\n            var maxLines = (int) Math.floor(maxHeight / font.lineHeight);\n            if (this.widget.ellipsize && !wrappedLines.isEmpty() && maxLines > 0 && (wrappedLines.size() > maxLines || font.width(wrappedLines.getLast()) > maxWidth)) {\n                wrappedLines = wrappedLines.subList(0, maxLines);\n\n                var ellipsis = FormattedText.of(\"…\");\n                var ellipsisLength = font.width(ellipsis);\n\n                var trimmedLastLine = font.substrByWidth(wrappedLines.getLast(), maxWidth - ellipsisLength);\n                wrappedLines.set(\n                    wrappedLines.size() - 1,\n                    FormattedText.composite(trimmedLastLine, ellipsis)\n                );\n            }\n\n            return Language.getInstance().getVisualOrder(wrappedLines);\n        }\n\n        protected TextMetrics measureText(Font font, List<FormattedCharSequence> lines) {\n            var textWidth = 0;\n            var textHeight = 0;\n            var lineWidths = new DoubleArrayList();\n\n            for (var line : lines) {\n                var lineWidth = font.width(line);\n                lineWidths.add(lineWidth);\n\n                textWidth = Math.max(textWidth, lineWidth);\n                textHeight += font.lineHeight;\n            }\n\n            return new TextMetrics(textWidth, textHeight, lineWidths);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var font = this.host().client().font;\n            this.renderText = this.wrapText(font, (int) constraints.maxWidth(), (int) constraints.maxHeight());\n\n            var metrics = this.measureText(font, this.renderText);\n\n            this.renderTextWidths = metrics.lineWidths();\n            this.renderTextHeight = metrics.height();\n\n            var size = Size.of(metrics.width, metrics.height).constrained(constraints);\n            this.transform.setSize(size);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            var renderer = this.host().client().font;\n            return this.measureText(renderer, this.wrapText(renderer, Integer.MAX_VALUE, (int) height)).width;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            var renderer = this.host().client().font;\n            return this.measureText(renderer, this.wrapText(renderer, this.widget.softWrap ? (int) width : Integer.MAX_VALUE, Integer.MAX_VALUE)).height;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.of(this.host().client().font.lineHeight - 2);\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            var font = this.host().client().font;\n            var yOffset = this.widget.style.textAlignment().alignVertical(this.transform.height(), this.renderTextHeight);\n\n            for (int lineIdx = 0; lineIdx < this.renderText.size(); lineIdx++) {\n                graphics.drawString(\n                    font,\n                    this.renderText.get(lineIdx),\n                    (int) this.widget.style.textAlignment().alignHorizontal(this.transform.width(), this.renderTextWidths.getDouble(lineIdx)),\n                    (int) yOffset + lineIdx * font.lineHeight,\n                    this.widget.style.baseColor().argb(),\n                    this.widget.style.shadow()\n                );\n            }\n        }\n\n        // this reimplementation of RawLabel.draw is pretty cringe, however\n        // mojang has left our hands tied since the text collector interface\n        // does not give us control over text color and shadow\n        public void collectText(ActiveTextCollector collector) {\n            var font = this.host().client().font;\n            var yOffset = this.widget.style.textAlignment().alignVertical(this.transform.height(), this.renderTextHeight);\n\n            for (int lineIdx = 0; lineIdx < this.renderText.size(); lineIdx++) {\n                collector.accept(\n                    (int) this.widget.style.textAlignment().alignHorizontal(this.transform.width(), this.renderTextWidths.getDouble(lineIdx)),\n                    (int) yOffset + lineIdx * font.lineHeight,\n                    this.renderText.get(lineIdx)\n                );\n            }\n        }\n\n        @Override\n        @Nullable\n        public List<ClientTooltipComponent> getTooltipComponentsAt(double x, double y) {\n            return null;\n        }\n\n        @Override\n        @Nullable\n        public Style getStyleAt(double x, double y) {\n            if (this.renderText.isEmpty()) return null;\n\n            var collector = new StyleCollector(this.host().client().font, (int) x, (int) y);\n            this.collectText(collector);\n\n            return collector.result();\n        }\n\n        @Override\n        public boolean onMouseDown(double x, double y, int button, KeyModifiers modifiers) {\n            if (button != 0) return MouseListener.super.onMouseDown(x, y, button, modifiers);\n            return this.textClickHandler.apply(this.getStyleAt(x, y));\n        }\n\n        @Override\n        public @Nullable CursorStyle cursorStyleAt(double x, double y) {\n            var style = this.getStyleAt(x, y);\n            if (style == null) return null;\n            if (style.getClickEvent() != null) return CursorStyle.HAND;\n            return null;\n        }\n\n        public static class StyleCollector extends ActiveTextCollector.ClickableStyleFinder {\n\n            public StyleCollector(Font font, int clickX, int clickY) {\n                super(font, clickX, clickY);\n                ((ClickableStyleFinderAccessor) this).owo$setStyleScanner(((ClickableStyleFinderAccessor) this)::owo$setResult);\n            }\n        }\n    }\n\n    public record TextMetrics(int width, int height, DoubleList lineWidths) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/object/BlockWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.object;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.mixin.ui.access.BlockEntityAccessor;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.world.level.block.EntityBlock;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.storage.TagValueInput;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix4f;\n\nimport java.util.Objects;\nimport java.util.function.Consumer;\n\npublic class BlockWidget extends StatefulWidget {\n\n    public final BlockState blockState;\n    public final @Nullable BlockEntity blockEntity;\n    public final @Nullable CompoundTag blockEntityNbt;\n    public final @Nullable Consumer<Matrix4f> transform;\n\n    private BlockWidget(BlockState blockState, @Nullable BlockEntity blockEntity, @Nullable CompoundTag blockEntityNbt, @Nullable Consumer<Matrix4f> transform) {\n        this.blockState = blockState;\n        this.blockEntity = blockEntity;\n        this.blockEntityNbt = blockEntityNbt;\n        this.transform = transform;\n    }\n\n    public BlockWidget(BlockState blockState, @Nullable BlockEntity blockEntity) {\n        this(blockState, blockEntity, null, null);\n    }\n\n    public BlockWidget(BlockState blockState, @Nullable BlockEntity blockEntity, Consumer<Matrix4f> transform) {\n        this(blockState, blockEntity, null, transform);\n    }\n\n    public BlockWidget(BlockState blockState, @Nullable CompoundTag blockEntityNbt) {\n        this(blockState, null, blockEntityNbt, null);\n    }\n\n    public BlockWidget(BlockState blockState, @Nullable CompoundTag blockEntityNbt, Consumer<Matrix4f> transform) {\n        this(blockState, null, blockEntityNbt, transform);\n    }\n\n    public BlockWidget(BlockState blockState, Consumer<Matrix4f> transform) {\n        this(blockState, null, null, transform);\n    }\n\n    public BlockWidget(BlockState blockState) {\n        this(blockState, null, null, null);\n    }\n\n    @Override\n    public WidgetState<BlockWidget> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<BlockWidget> {\n\n        private @Nullable BlockEntity internalBlockEntity;\n\n        @Override\n        public void init() {\n            this.resetBlockEntity();\n        }\n\n        @Override\n        public void didUpdateWidget(BlockWidget oldWidget) {\n            if (this.widget().blockState == oldWidget.blockState\n                && this.widget().blockEntity == oldWidget.blockEntity\n                && Objects.equals(this.widget().blockEntityNbt, oldWidget.blockEntityNbt)) {\n                return;\n            }\n\n            this.resetBlockEntity();\n        }\n\n        private void resetBlockEntity() {\n            this.internalBlockEntity = this.widget().blockEntity == null\n                ? prepareBlockEntity(this.widget().blockState, this.widget().blockEntityNbt)\n                : null;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new RawBlockWidget(\n                this.widget().blockState,\n                this.internalBlockEntity != null ? this.internalBlockEntity : this.widget().blockEntity,\n                this.widget().transform\n            );\n        }\n\n        // ---\n\n        private static @Nullable BlockEntity prepareBlockEntity(BlockState state, @Nullable CompoundTag nbt) {\n            var client = Minecraft.getInstance();\n            if (!state.hasBlockEntity()) {\n                return null;\n            }\n\n            var blockEntity = ((EntityBlock) state.getBlock()).newBlockEntity(client.player.blockPosition(), state);\n            if (blockEntity == null) {\n                return null;\n            }\n\n            ((BlockEntityAccessor) blockEntity).owo$setBlockState(state);\n            blockEntity.setLevel(client.level);\n\n            if (nbt != null) {\n                blockEntity.loadWithComponents(TagValueInput.create(new ProblemReporter.ScopedCollector(Owo.LOGGER), client.level.registryAccess(), nbt));\n            }\n\n            return blockEntity;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/object/EntityWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.object;\n\nimport com.mojang.math.Axis;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.element.BraidEntityElement;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.LivingEntity;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix4f;\nimport org.joml.Vector4f;\n\nimport java.util.OptionalDouble;\nimport java.util.function.Consumer;\n\npublic class EntityWidget extends LeafInstanceWidget {\n\n    public final double scale;\n    public final Entity entity;\n\n    protected DisplayMode displayMode = DisplayMode.FIXED;\n    protected boolean scaleToFit = true;\n    protected boolean showNametag = false;\n    protected @Nullable Consumer<Matrix4f> transform = null;\n\n    public EntityWidget(double scale, Entity entity, @Nullable WidgetSetupCallback<EntityWidget> setupCallback) {\n        this.scale = scale;\n        this.entity = entity;\n        if (setupCallback != null) setupCallback.setup(this);\n    }\n\n    public EntityWidget displayMode(DisplayMode displayMode) {\n        this.displayMode = displayMode;\n        return this;\n    }\n\n    public DisplayMode displayMode() {\n        return this.displayMode;\n    }\n\n    public EntityWidget scaleToFit(boolean scaleToFit) {\n        this.scaleToFit = scaleToFit;\n        return this;\n    }\n\n    public boolean scaleToFit() {\n        return this.scaleToFit;\n    }\n\n    public EntityWidget showNametag(boolean showNametag) {\n        this.showNametag = showNametag;\n        return this;\n    }\n\n    public boolean showNametag() {\n        return this.showNametag;\n    }\n\n    public EntityWidget transform(Consumer<Matrix4f> transform) {\n        this.transform = transform;\n        return this;\n    }\n\n    public @Nullable Consumer<Matrix4f> transform() {\n        return this.transform;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends LeafWidgetInstance<EntityWidget> {\n\n        protected double baseScale = 1.0;\n\n        public Instance(EntityWidget widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(EntityWidget widget) {\n            if (this.widget.scaleToFit != widget.scaleToFit) {\n                this.markNeedsLayout();\n            }\n\n            super.setWidget(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            this.transform.setSize(constraints.minSize());\n\n            if (this.widget.scaleToFit) {\n                this.baseScale = Math.min(\n                    this.transform.width() / this.widget.entity.getBbWidth(),\n                    this.transform.height() / this.widget.entity.getBbHeight()\n                ) * .6;\n            }\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return 32;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return 32;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            var entity = this.widget.entity;\n\n            var entitySpaceToWidgetSpace = new Matrix4f();\n            entitySpaceToWidgetSpace.translate(0, (float) (this.transform.height() / 2), 100);\n            entitySpaceToWidgetSpace.scale((float) (this.widget.scale * this.baseScale));\n            entitySpaceToWidgetSpace.scale(1, -1, -1);\n\n            var entityTransform = new Matrix4f();\n            if (this.widget.transform != null) {\n                this.widget.transform.accept(entityTransform);\n            }\n\n            entityTransform.translate(0, -entity.getBbHeight() / 2, 0);\n\n            var xRotation = 0f;\n            var yRotation = 0f;\n\n            var lastHeadYaw = entity instanceof LivingEntity living ? living.yHeadRotO : 0;\n            var lastYaw = entity.yRotO;\n            var lastPitch = entity.xRotO;\n\n            if (this.widget.displayMode == DisplayMode.FIXED) {\n                xRotation = 35;\n                yRotation = -45;\n            } else if (this.widget.displayMode != DisplayMode.NONE) {\n                var globalCursorPos = this.host().cursorPosition();\n                var cursor4x4Buffer = graphics.pose().get4x4(new float[16]);\n\n                var cursorTransform = new Matrix4f()\n                    .set(cursor4x4Buffer)\n                    // we do this ugly cursor-specific offset here to account for the\n                    // centering being indiscriminately applied inside the PIP renderer\n                    .translate((float) (this.transform.width() / 2), 0, 0)\n                    .mul(entitySpaceToWidgetSpace)\n                    .mul(entityTransform)\n                    .invert();\n\n                var localCursorPos = cursorTransform.transform(new Vector4f((float) globalCursorPos.x(), (float) globalCursorPos.y(), 0, 1));\n\n                switch (widget.displayMode) {\n                    case CURSOR -> {\n                        var center = new Vector4f(0, entity.getEyeHeight(entity.getPose()), 0, 1);\n\n                        xRotation = (float) Math.toDegrees(Math.atan(localCursorPos.y - center.y)) * -.15f;\n                        yRotation = (float) Math.toDegrees(Math.atan(localCursorPos.x - center.x)) * .15f;\n                        if (entity instanceof LivingEntity living) living.yHeadRotO = -yRotation * 3;\n\n                        entity.yRotO = -yRotation * .65f;\n                        entity.xRotO = xRotation * 2.5f;\n                    }\n                    case VANILLA -> {\n                        var center = new Vector4f(0, entity.getBbHeight() / 2, 0, 1);\n\n                        xRotation = (float) Math.atan(localCursorPos.y - center.y) * -20f;\n                        yRotation = (float) Math.atan(localCursorPos.x - center.x) * 20f;\n                        if (entity instanceof LivingEntity living) living.yHeadRotO = -yRotation;\n\n                        entity.yRotO = -yRotation;\n                        entity.xRotO = xRotation;\n                    }\n                }\n            }\n\n            // We make sure the yRotation never becomes 0, as the lighting otherwise becomes very unhappy\n            if (yRotation == 0) yRotation = .1f;\n\n            entityTransform.rotate(Axis.XP.rotationDegrees(xRotation));\n            entityTransform.rotate(Axis.YP.rotationDegrees(yRotation));\n\n            var entityState = this.host().client().getEntityRenderDispatcher().extractEntity(this.widget.entity, 0);\n\n            if (!this.widget.showNametag) {\n                entityState.nameTag = null;\n            }\n\n            graphics.guiRenderState.submitPicturesInPictureState(new BraidEntityElement(\n                entityState,\n                new Matrix4f().mul(entitySpaceToWidgetSpace).mul(entityTransform),\n                new Matrix3x2f(graphics.pose()),\n                this.transform.width(), this.transform.height(),\n                graphics.scissorStack.peek()\n            ));\n\n            if (entity instanceof LivingEntity living) living.yHeadRotO = lastHeadYaw;\n            entity.xRotO = lastPitch;\n            entity.yRotO = lastYaw;\n        }\n    }\n\n    public enum DisplayMode {\n        FIXED, VANILLA, CURSOR, NONE\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/object/ItemStackWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.object;\n\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.core.element.BraidItemElement;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport net.minecraft.client.renderer.item.ItemStackRenderState;\nimport net.minecraft.world.item.ItemDisplayContext;\nimport net.minecraft.world.item.ItemStack;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix4f;\n\nimport java.util.OptionalDouble;\nimport java.util.function.Consumer;\n\n/// A widget that renders an [ItemStack]\n///\n/// The stack is rendered using the specified [ItemDisplayContext]\n/// and can show overlay information (item bar, count, cooldown progress, etc.)\npublic class ItemStackWidget extends LeafInstanceWidget {\n\n    public final ItemStack stack;\n    protected boolean showOverlay = true;\n    protected ItemDisplayContext displayContext = ItemDisplayContext.GUI;\n    protected @Nullable LightOverride lightOverride = null;\n    protected @Nullable Consumer<Matrix4f> transform;\n\n    public ItemStackWidget(ItemStack stack, @Nullable WidgetSetupCallback<ItemStackWidget> setupCallback) {\n        this.stack = stack;\n        if (setupCallback != null) setupCallback.setup(this);\n    }\n\n    public ItemStackWidget(ItemStack stack) {\n        this(stack, null);\n    }\n\n    public ItemStackWidget showOverlay(boolean showOverlay) {\n        this.assertMutable();\n        this.showOverlay = showOverlay;\n        return this;\n    }\n\n    public boolean showOverlay() {\n        return this.showOverlay;\n    }\n\n    public ItemStackWidget displayContext(ItemDisplayContext displayContext) {\n        this.assertMutable();\n        this.displayContext = displayContext;\n        this.showOverlay = false;\n        return this;\n    }\n\n    public ItemDisplayContext displayContext() {\n        return this.displayContext;\n    }\n\n    public ItemStackWidget lightOverride(@Nullable LightOverride lightOverride) {\n        this.assertMutable();\n        this.lightOverride = lightOverride;\n        return this;\n    }\n\n    public @Nullable LightOverride lightOverride() {\n        return this.lightOverride;\n    }\n\n    public ItemStackWidget transform(@Nullable Consumer<Matrix4f> transform) {\n        this.transform = transform;\n        return this;\n    }\n\n    public @Nullable Consumer<Matrix4f> transform() {\n        return this.transform;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends LeafWidgetInstance<ItemStackWidget> {\n\n        public static final Size DEFAULT_SIZE = Size.square(16);\n\n        public Instance(ItemStackWidget widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var size = DEFAULT_SIZE.constrained(constraints);\n            this.transform.setSize(size);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return 16;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return 16;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            if (this.transform.width() <= 16 && this.transform.height() <= 16 && this.widget.displayContext == ItemDisplayContext.GUI && this.widget.transform == null) {\n                // scale according to widget size, since items assume a 16x16 window\n                graphics.push().scale((float) (this.transform.width() / 16f), (float) (this.transform.height() / 16f));\n                graphics.renderItem(this.widget.stack, 0, 0);\n                graphics.pop();\n            } else {\n                var state = new ItemStackRenderState();\n                this.host().client().getItemModelResolver().appendItemLayers(state, this.widget.stack, this.widget.displayContext, this.host().client().level, this.host().client().player, 0);\n\n                var transformThisFrame = new Matrix4f();\n                if (this.widget.transform != null) {\n                    this.widget.transform.accept(transformThisFrame);\n                }\n\n                graphics.guiRenderState.submitPicturesInPictureState(new BraidItemElement(\n                    state,\n                    this.transform.width(),\n                    this.transform.height(),\n                    graphics.scissorStack.peek(),\n                    transformThisFrame,\n                    new Matrix3x2f(graphics.pose())\n                ));\n            }\n\n            if (this.widget.showOverlay) {\n                var popTransform = false;\n                if (this.transform.width() != 16 || this.transform.height() != 16) {\n                    popTransform = true;\n\n                    graphics.push();\n                    graphics.scale((float) (this.transform.width() / 16), (float) (this.transform.height() / 16));\n                }\n\n                graphics.renderItemDecorations(this.host().client().font, this.widget.stack, 0, 0);\n\n                if (popTransform) {\n                    graphics.pop();\n                }\n            }\n        }\n    }\n\n    public enum LightOverride {\n        FRONT,\n        SIDE\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/object/RawBlockWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.object;\n\nimport com.mojang.math.Axis;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.core.element.BraidBlockElement;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix4f;\n\nimport java.util.OptionalDouble;\nimport java.util.function.Consumer;\n\n/// A widget that renders a [BlockState] and optionally a [BlockEntity]\npublic class RawBlockWidget extends LeafInstanceWidget {\n\n    public final BlockState blockState;\n    public final @Nullable BlockEntity blockEntity;\n    public final @Nullable Consumer<Matrix4f> transform;\n\n    public RawBlockWidget(BlockState blockState, @Nullable BlockEntity blockEntity, @Nullable Consumer<Matrix4f> transform) {\n        this.blockState = blockState;\n        this.blockEntity = blockEntity;\n        this.transform = transform;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    // ---\n\n    public static class Instance extends LeafWidgetInstance<RawBlockWidget> {\n\n        public static final Size DEFAULT_SIZE = Size.square(16);\n\n        public Instance(RawBlockWidget widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var size = DEFAULT_SIZE.constrained(constraints);\n            this.transform.setSize(size);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return 16;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return 16;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            var drawTransform = new Matrix4f();\n            drawTransform.scale(40 * (float) (this.transform.width() / 64f), -40 * (float) (this.transform.height() / 64f), -40);\n\n            if (this.widget.transform != null) {\n                this.widget.transform.accept(drawTransform);\n            } else {\n                drawTransform.rotate(Axis.XP.rotationDegrees(30));\n                drawTransform.rotate(Axis.YP.rotationDegrees(45 + 180));\n            }\n\n            drawTransform.translate(-.5f, -.5f, -.5f);\n\n            BlockEntityRenderState entity = null;\n            if (this.widget.blockEntity != null) {\n                var renderer = Minecraft.getInstance().getBlockEntityRenderDispatcher().getRenderer(this.widget.blockEntity);\n                if (renderer != null) {\n                    entity = renderer.createRenderState();\n                    renderer.extractRenderState(\n                        this.widget.blockEntity, entity, 0, Vec3.ZERO, null\n                    );\n                }\n            }\n\n            graphics.guiRenderState.submitPicturesInPictureState(new BraidBlockElement(\n                this.widget.blockState,\n                entity,\n                drawTransform,\n                new Matrix3x2f(graphics.pose()),\n                this.transform.width(),\n                this.transform.height(),\n                graphics.scissorStack.peek()\n            ));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/overlay/Overlay.java",
    "content": "package io.wispforest.owo.braid.widgets.overlay;\n\nimport com.google.common.base.Preconditions;\nimport com.google.common.collect.Iterables;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.Key;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.EmptyWidget;\nimport io.wispforest.owo.braid.widgets.basic.HitTestTrap;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Overlay extends StatefulWidget {\n\n    public final Widget child;\n\n    public Overlay(Widget child) {\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<Overlay> createState() {\n        return new State();\n    }\n\n    // ---\n\n    public static @Nullable State maybeOf(BuildContext context) {\n        var provider = context.getAncestor(OverlayProvider.class);\n        return provider != null ? provider.state : null;\n    }\n\n    public static State of(BuildContext context) {\n        var state = maybeOf(context);\n        Preconditions.checkNotNull(state, \"attempted to look up the enclosing overlay state without one present\");\n\n        return state;\n    }\n\n    // ---\n\n    public static class State extends WidgetState<Overlay> {\n\n        public OverlayEntry add(OverlayEntryBuilder builder) {\n            var entryPosition = builder.position.convertTo(this.context());\n\n            var entry = new OverlayEntry(\n                this,\n                builder.onRemove,\n                builder.widget,\n                builder.dismissOverlayOnClick,\n                builder.occludeHitTest,\n                entryPosition.x,\n                entryPosition.y\n            );\n\n            this.setState(() -> {\n                this.entries.add(entry);\n            });\n\n            return entry;\n        }\n\n        // ---\n\n        final List<OverlayEntry> entries = new ArrayList<>();\n\n        @SuppressWarnings(\"DataFlowIssue\")\n        @Override\n        public Widget build(BuildContext context) {\n            return new OverlayProvider(\n                this,\n                new Stack(\n                    this.widget().child,\n                    new HitTestTrap(\n                        Iterables.any(this.entries, entry -> entry.occludeHitTest),\n                        new MouseArea(\n                            widget -> widget\n                                .clickCallback((x, y, button, modifiers) -> {\n                                    if (!Iterables.any(this.entries, entry -> entry.dismissOnOverlayClick)) return false;\n\n                                    for (var entry : Iterables.filter(this.entries, entry -> entry.dismissOnOverlayClick)) {\n                                        if (entry.onRemove != null) entry.onRemove.run();\n                                    }\n\n                                    this.setState(() -> {\n                                        this.entries.removeIf(entry -> entry.dismissOnOverlayClick);\n                                    });\n\n                                    return false;\n                                }),\n                            EmptyWidget.INSTANCE\n                        )\n                    ),\n                    new StackBase(\n                        new RawOverlay(\n                            this.entries.stream()\n                                .map(entry -> (RawOverlayElement) new RawOverlayElement(entry.x, entry.y, entry.widget).key(Key.of(entry.uuid.toString())))\n                                .toList()\n                        )\n                    )\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/overlay/OverlayEntry.java",
    "content": "package io.wispforest.owo.braid.widgets.overlay;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.UUID;\n\npublic class OverlayEntry {\n\n    private final Overlay.State owner;\n    final @Nullable Runnable onRemove;\n    final UUID uuid = UUID.randomUUID();\n\n    public Widget widget;\n    public boolean dismissOnOverlayClick;\n    public boolean occludeHitTest;\n\n    public double x;\n    public double y;\n\n    OverlayEntry(Overlay.State owner, @Nullable Runnable onRemove, Widget widget, boolean dismissOnOverlayClick, boolean occludeHitTest, double x, double y) {\n        this.owner = owner;\n        this.onRemove = onRemove;\n        this.widget = widget;\n        this.dismissOnOverlayClick = dismissOnOverlayClick;\n        this.occludeHitTest = occludeHitTest;\n        this.x = x;\n        this.y = y;\n    }\n\n    // ---\n\n    public void setState(Runnable fn) {\n        this.owner.setState(fn);\n    }\n\n    public void remove() {\n        this.owner.setState(() -> {\n            if (this.onRemove != null) this.onRemove.run();\n            this.owner.entries.remove(this);\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/overlay/OverlayEntryBuilder.java",
    "content": "package io.wispforest.owo.braid.widgets.overlay;\n\nimport io.wispforest.owo.braid.core.RelativePosition;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class OverlayEntryBuilder {\n\n    final Widget widget;\n    final RelativePosition position;\n    @Nullable Runnable onRemove = null;\n    boolean dismissOverlayOnClick = false;\n    boolean occludeHitTest = false;\n\n    public OverlayEntryBuilder(Widget widget, RelativePosition position) {\n        this.widget = widget;\n        this.position = position;\n    }\n\n    public OverlayEntryBuilder onRemove(Runnable onRemove) {\n        this.onRemove = onRemove;\n        return this;\n    }\n\n    public OverlayEntryBuilder dismissOverlayOnClick() {\n        this.dismissOverlayOnClick = true;\n        return this;\n    }\n\n    public OverlayEntryBuilder occludeHitTest() {\n        this.occludeHitTest = true;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/overlay/OverlayParentData.java",
    "content": "package io.wispforest.owo.braid.widgets.overlay;\n\npublic class OverlayParentData {\n    public double x, y;\n    public OverlayParentData(double x, double y) {\n        this.x = x;\n        this.y = y;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/overlay/OverlayProvider.java",
    "content": "package io.wispforest.owo.braid.widgets.overlay;\n\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nclass OverlayProvider extends InheritedWidget {\n\n    public final Overlay.State state;\n\n    protected OverlayProvider(Overlay.State state, Widget child) {\n        super(child);\n        this.state = state;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/overlay/RawOverlay.java",
    "content": "package io.wispforest.owo.braid.widgets.overlay;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.OptionalDouble;\n\npublic class RawOverlay extends MultiChildInstanceWidget {\n\n    public RawOverlay(List<? extends RawOverlayElement> children) {\n        super(children);\n    }\n\n    public RawOverlay(RawOverlayElement... children) {\n        this(Arrays.asList(children));\n    }\n\n    @Override\n    public MultiChildWidgetInstance<RawOverlay> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends MultiChildWidgetInstance<RawOverlay> {\n\n        public Instance(RawOverlay widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            for (var child : this.children) {\n                child.layout(Constraints.unconstrained());\n\n                var parentData = (OverlayParentData) child.parentData;\n                child.transform.setX(parentData.x);\n                child.transform.setY(parentData.y);\n            }\n\n            this.transform.setSize(constraints.maxFiniteOrMinSize());\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return 0;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return 0;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/overlay/RawOverlayElement.java",
    "content": "package io.wispforest.owo.braid.widgets.overlay;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.VisitorWidget;\n\npublic class RawOverlayElement extends VisitorWidget {\n    public final double x, y;\n\n    public RawOverlayElement(double x, double y, Widget child) {\n        super(child);\n        this.x = x;\n        this.y = y;\n    }\n\n    public static final Visitor<RawOverlayElement> VISITOR = (widget, instance) -> {\n        if (instance.parentData instanceof OverlayParentData data) {\n            data.x = widget.x;\n            data.y = widget.y;\n        } else {\n            instance.parentData = new OverlayParentData(widget.x, widget.y);\n        }\n\n        instance.markNeedsLayout();\n    };\n\n    @Override\n    public Proxy<?> proxy() {\n        return new Proxy<>(this, VISITOR);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/owoui/OwoUIWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.owoui;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Align;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\n\nimport java.util.function.Supplier;\n\npublic class OwoUIWidget extends StatefulWidget {\n    private final Supplier<ParentUIComponent> componentSupplier;\n\n    public OwoUIWidget(Supplier<ParentUIComponent> componentSupplier) {\n        this.componentSupplier = componentSupplier;\n    }\n\n    @Override\n    public WidgetState<?> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<OwoUIWidget> {\n        private ParentUIComponent component;\n        private BuildContext owoUiContext;\n\n        @Override\n        public void init() {\n            component = this.widget().componentSupplier.get();\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Align(\n                Alignment.TOP_LEFT,\n                new Focusable(\n                    widget -> widget\n                        .focusLostCallback(() -> ((OwoUIWidgetWrapper.Instance) this.owoUiContext.instance()).onFocusLost())\n                        .keyDownCallback((keyCode, modifiers) -> ((OwoUIWidgetWrapper.Instance) this.owoUiContext.instance()).onKeyDown(keyCode, modifiers))\n                        .charCallback((charCode, modifiers) -> ((OwoUIWidgetWrapper.Instance) this.owoUiContext.instance()).onChar(charCode, modifiers)),\n                    new Builder(owoUiContext -> {\n                        this.owoUiContext = owoUiContext;\n                        return new OwoUIWidgetWrapper(component);\n                    })\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/owoui/OwoUIWidgetWrapper.java",
    "content": "package io.wispforest.owo.braid.widgets.owoui;\n\nimport com.mojang.blaze3d.opengl.GlStateManager;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.MouseListener;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.input.MouseButtonInfo;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.OptionalDouble;\n\npublic class OwoUIWidgetWrapper extends LeafInstanceWidget {\n    private final ParentUIComponent rootComponent;\n\n    public OwoUIWidgetWrapper(ParentUIComponent rootComponent) {\n        this.rootComponent = rootComponent;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends LeafWidgetInstance<OwoUIWidgetWrapper> implements MouseListener {\n        private int mouseX = -100;\n        private int mouseY = -100;\n\n        private int dragButton = -1;\n\n        public Instance(OwoUIWidgetWrapper widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            Size space = constraints.maxFiniteOrMinSize();\n\n            widget.rootComponent.inflate(io.wispforest.owo.ui.core.Size.of((int) space.width(), (int) space.height()));\n            widget.rootComponent.mount(null, 0, 0);\n\n            this.transform.setSize(Size.of(widget.rootComponent.width(), widget.rootComponent.height())\n                .constrained(constraints));\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            // the focus handler is created on mount, therefore it's null only if the\n            // component wasn't mounted.\n            if (widget.rootComponent.focusHandler() != null) {\n                throw new IllegalStateException(\"Tried to measure intrinsic width of mounted owo-ui component\");\n            }\n\n            widget.rootComponent.inflate(io.wispforest.owo.ui.core.Size.of(Integer.MAX_VALUE, (int) height));\n            return widget.rootComponent.width();\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            // the focus handler is created on mount, therefore it's null only if the\n            // component wasn't mounted.\n            if (widget.rootComponent.focusHandler() != null) {\n                throw new IllegalStateException(\"Tried to measure intrinsic height of mounted owo-ui component\");\n            }\n\n            widget.rootComponent.inflate(io.wispforest.owo.ui.core.Size.of((int) width, Integer.MAX_VALUE));\n            return widget.rootComponent.height();\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n\n        @Override\n        public void onMouseMove(double toX, double toY) {\n            this.mouseX = (int) toX;\n            this.mouseY = (int) toY;\n        }\n\n        @Override\n        public void onMouseExit() {\n            this.mouseX = -100;\n            this.mouseY = -100;\n        }\n\n        public void onFocusLost() {\n            this.widget.rootComponent.focusHandler().focus(null, UIComponent.FocusSource.MOUSE_CLICK);\n        }\n\n        @Override\n        public @Nullable CursorStyle cursorStyleAt(double x, double y) {\n            var hovered = this.widget.rootComponent.childAt((int) x, (int) y);\n\n            if (hovered == null) return null;\n\n            return switch (hovered.cursorStyle()) {\n                case NONE -> CursorStyle.NONE;\n                case POINTER -> CursorStyle.POINTER;\n                case TEXT -> CursorStyle.TEXT;\n                case HAND -> CursorStyle.HAND;\n                case CROSSHAIR -> CursorStyle.CROSSHAIR;\n                case MOVE -> CursorStyle.MOVE;\n                case HORIZONTAL_RESIZE -> CursorStyle.HORIZONTAL_RESIZE;\n                case VERTICAL_RESIZE -> CursorStyle.VERTICAL_RESIZE;\n                case NWSE_RESIZE -> CursorStyle.NWSE_RESIZE;\n                case NESW_RESIZE -> CursorStyle.NESW_RESIZE;\n                case NOT_ALLOWED -> CursorStyle.NOT_ALLOWED;\n            };\n        }\n\n        @Override\n        public boolean onMouseDown(double x, double y, int button, KeyModifiers modifiers) {\n            return this.widget.rootComponent.onMouseDown(new MouseButtonEvent(x, y, new MouseButtonInfo(button, modifiers.bitMask())), false);\n        }\n\n        @Override\n        public boolean onMouseUp(double x, double y, int button, KeyModifiers modifiers) {\n            return this.widget.rootComponent.onMouseUp(new MouseButtonEvent(x, y, new MouseButtonInfo(button, modifiers.bitMask())));\n        }\n\n        @Override\n        public boolean onMouseScroll(double x, double y, double horizontal, double vertical) {\n            return this.widget.rootComponent.onMouseScroll(x, y, vertical);\n        }\n\n        @Override\n        public void onMouseDragStart(int button, KeyModifiers modifiers) {\n            this.dragButton = button;\n        }\n\n        @Override\n        public void onMouseDrag(double x, double y, double dx, double dy) {\n            this.widget.rootComponent.onMouseDrag(new MouseButtonEvent(x, y, new MouseButtonInfo(this.dragButton, 0)), dx, dy);\n        }\n\n        @Override\n        public void onMouseDragEnd() {\n            this.dragButton = -1;\n        }\n\n        public boolean onKeyDown(int keyCode, KeyModifiers modifiers) {\n            return this.widget.rootComponent.onKeyPress(new KeyEvent(keyCode, GLFW.glfwGetKeyScancode(keyCode), modifiers.bitMask()));\n        }\n\n        public boolean onChar(int charCode, KeyModifiers modifiers) {\n            return this.widget.rootComponent.onCharTyped(new CharacterEvent(charCode, modifiers.bitMask()));\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            var client = host().client();\n\n            this.widget.rootComponent.update(\n                client.getDeltaTracker().getGameTimeDeltaTicks(),\n                mouseX,\n                mouseY\n            );\n\n            this.widget.rootComponent.draw(\n                graphics,\n                mouseX,\n                mouseY,\n                client.getDeltaTracker().getGameTimeDeltaPartialTick(false),\n                client.getDeltaTracker().getGameTimeDeltaTicks()\n            );\n\n            // TODO: tooltips.\n\n            // this mitigates the vanilla scissor stack disabling the scissor stack if it's empty\n            GlStateManager._enableScissorTest();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/recipeviewer/RecipeViewerExclusionZone.java",
    "content": "package io.wispforest.owo.braid.widgets.recipeviewer;\n\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\npublic class RecipeViewerExclusionZone extends SingleChildInstanceWidget {\n    public RecipeViewerExclusionZone(Widget child) {\n        super(child);\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<RecipeViewerExclusionZone> {\n        public Instance(RecipeViewerExclusionZone widget) {\n            super(widget);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/recipeviewer/RecipeViewerStack.java",
    "content": "package io.wispforest.owo.braid.widgets.recipeviewer;\n\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.util.ViewerStack;\n\nimport java.util.function.Supplier;\n\npublic class RecipeViewerStack extends SingleChildInstanceWidget {\n    public final Supplier<ViewerStack> stackProvider;\n\n    public RecipeViewerStack(Supplier<ViewerStack> stackProvider, Widget child) {\n        super(child);\n        this.stackProvider = stackProvider;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<RecipeViewerStack> {\n        public Instance(RecipeViewerStack widget) {\n            super(widget);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/recipeviewer/StackDropArea.java",
    "content": "package io.wispforest.owo.braid.widgets.recipeviewer;\n\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.util.ViewerStack;\n\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\n\npublic class StackDropArea extends SingleChildInstanceWidget {\n    public final Predicate<ViewerStack> stackPredicate;\n    public final Consumer<ViewerStack> stackAcceptor;\n\n    public StackDropArea(Predicate<ViewerStack> stackPredicate, Consumer<ViewerStack> stackAcceptor, Widget child) {\n        super(child);\n        this.stackPredicate = stackPredicate;\n        this.stackAcceptor = stackAcceptor;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance.ShrinkWrap<StackDropArea> {\n        public Instance(StackDropArea widget) {\n            super(widget);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/ButtonScrollbar.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.owo.ui.component.ButtonComponent;\n\npublic class ButtonScrollbar extends Scrollbar {\n\n    public ButtonScrollbar(LayoutAxis axis, ScrollController controller) {\n        super(\n            axis,\n            controller,\n            new Panel(ButtonComponent.DISABLED_TEXTURE),\n            new Panel(ButtonComponent.ACTIVE_TEXTURE)\n        );\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/DefaultScrollAnimationSettings.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class DefaultScrollAnimationSettings extends InheritedWidget {\n\n    public final ScrollAnimationSettings settings;\n\n    public DefaultScrollAnimationSettings(ScrollAnimationSettings settings, Widget child) {\n        super(child);\n        this.settings = settings;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return this.settings != ((DefaultScrollAnimationSettings) newWidget).settings;\n    }\n\n    // ---\n\n    public static @Nullable ScrollAnimationSettings maybeOf(BuildContext context) {\n        var widget = context.dependOnAncestor(DefaultScrollAnimationSettings.class);\n        if (widget != null) {\n            return widget.settings;\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/FlatScrollbar.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.HoverableBuilder;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\n\npublic class FlatScrollbar extends Scrollbar {\n    public FlatScrollbar(LayoutAxis axis, ScrollController controller, Color color, Color hoveredColor) {\n        super(\n            axis,\n            controller,\n            new Padding(Insets.none()),\n            new HoverableBuilder(\n                new Box(color),\n                new Box(hoveredColor)\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/HorizontallyScrollable.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class HorizontallyScrollable extends Scrollable {\n    public HorizontallyScrollable(@Nullable ScrollController controller, @Nullable ScrollAnimationSettings animationSettings, Widget child) {\n        super(true, false, controller, null, animationSettings, child);\n    }\n\n    public HorizontallyScrollable(Widget child) {\n        this(null, null, child);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/RawScrollView.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.OptionalDouble;\n\npublic class RawScrollView extends SingleChildInstanceWidget {\n\n    public final ScrollController horizontalController;\n    public final ScrollController verticalController;\n\n    public RawScrollView(\n        @Nullable ScrollController horizontalController,\n        @Nullable ScrollController verticalController,\n        Widget child\n    ) {\n        super(child);\n        this.horizontalController = horizontalController;\n        this.verticalController = verticalController;\n    }\n\n    @Override\n    public SingleChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends SingleChildWidgetInstance<RawScrollView> {\n\n        protected double horizontalOffset, maxHorizontalOffset;\n        protected double verticalOffset, maxVerticalOffset;\n\n        public Instance(RawScrollView widget) {\n            super(widget);\n\n            this.horizontalOffset = this.widget.horizontalController != null ? this.widget.horizontalController.offset() : 0;\n            this.maxHorizontalOffset = this.widget.horizontalController != null ? this.widget.horizontalController.maxOffset() : 0;\n            this.verticalOffset = this.widget.verticalController != null ? this.widget.verticalController.offset() : 0;\n            this.maxVerticalOffset = this.widget.verticalController != null ? this.widget.verticalController.maxOffset() : 0;\n        }\n\n        @Override\n        public void setWidget(RawScrollView widget) {\n            var horizontalOffset = this.widget.horizontalController != null ? this.widget.horizontalController.offset() : 0;\n            var maxHorizontalOffset = this.widget.horizontalController != null ? this.widget.horizontalController.maxOffset() : 0;\n            var verticalOffset = this.widget.verticalController != null ? this.widget.verticalController.offset() : 0;\n            var maxVerticalOffset = this.widget.verticalController != null ? this.widget.verticalController.maxOffset() : 0;\n\n            if (!(this.horizontalOffset == horizontalOffset\n                && this.maxHorizontalOffset == maxHorizontalOffset\n                && this.verticalOffset == verticalOffset\n                && this.maxVerticalOffset == maxVerticalOffset)) {\n\n                this.horizontalOffset = horizontalOffset;\n                this.maxHorizontalOffset = maxHorizontalOffset;\n                this.verticalOffset = verticalOffset;\n                this.maxVerticalOffset = maxVerticalOffset;\n\n                this.markNeedsLayout();\n            }\n\n            super.setWidget(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var childSize = this.child.layout(\n                Constraints.of(\n                    constraints.minWidth(),\n                    constraints.minHeight(),\n                    this.widget.horizontalController != null ? Double.POSITIVE_INFINITY : constraints.maxWidth(),\n                    this.widget.verticalController != null ? Double.POSITIVE_INFINITY : constraints.maxHeight()\n                )\n            );\n\n            var selfSize = childSize.constrained(constraints);\n\n            this.updateMaxOffset(this.widget.horizontalController, Math.max(0, childSize.width() - selfSize.width()));\n            this.updateMaxOffset(this.widget.verticalController, Math.max(0, childSize.height() - selfSize.height()));\n\n            this.child.transform.setX(-this.horizontalOffset);\n            this.child.transform.setY(-this.verticalOffset);\n\n            this.transform.setSize(selfSize);\n        }\n\n        /// Delay the actual invocation of scroll controller listeners until\n        /// after the current layout cycle.\n        ///\n        /// This is important, because for one nobody could react to it anyways\n        /// (since we are in the layout phase, the build phase for this frame\n        /// is over) but *also* it actually breaks instances which descend from\n        /// a layout builder. This happens because such a descendant would now\n        /// mark itself dirty during the layout phase, but before the layout builder\n        /// instance is marked clean. Thus, the `markNeedsLayout()` invocation on\n        /// that layout builder instance gets swallowed and the widget is now stuck\n        /// in improperly-rebuilt limbo until the layout builder happens to re-layout\n        /// for other reasons. That is especially problematic because there is\n        /// potential for this effect to mask legitimate rebuilds said descendant\n        /// requires - it won't mark itself as needing a rebuild again because it\n        /// is still dutifully waiting for such a rebuild to occur.\n        private void updateMaxOffset(@Nullable ScrollController controller, double offset) {\n            if (controller == null) return;\n\n            if (controller.setMaxOffset(offset) && !controller.maxOffsetNotificationScheduled) {\n                controller.maxOffsetNotificationScheduled = true;\n                this.host().schedulePostLayoutCallback(controller::sendMaxOffsetNotification);\n            }\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return this.widget.horizontalController == null ? this.child.getIntrinsicWidth(height) : 0;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return this.widget.verticalController == null ? this.child.getIntrinsicHeight(width) : 0;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            var childBaseline = this.child != null ? this.child.getBaselineOffset() : OptionalDouble.empty();\n            if (childBaseline.isEmpty()) return OptionalDouble.empty();\n\n            return OptionalDouble.of(childBaseline.getAsDouble() + this.child.transform.y());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/ScrollAnimationSettings.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.animation.Easing;\n\nimport java.time.Duration;\n\npublic record ScrollAnimationSettings(Duration duration, Easing easing) {\n    public static final ScrollAnimationSettings DEFAULT = new ScrollAnimationSettings(Duration.ofMillis(250), Easing.OUT_QUART);\n    public static final ScrollAnimationSettings NO_ANIMATION = new ScrollAnimationSettings(null, null);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/ScrollController.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.animation.Animation;\nimport io.wispforest.owo.braid.animation.DoubleLerp;\nimport io.wispforest.owo.braid.animation.Easing;\nimport io.wispforest.owo.braid.core.Listenable;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport net.minecraft.util.Mth;\n\nimport java.time.Duration;\n\npublic class ScrollController extends Listenable {\n\n    private final Animation animation;\n    private DoubleLerp lerp;\n\n    public ScrollController(WidgetState<?> contextState) {\n        this(contextState::scheduleAnimationCallback);\n    }\n\n    public ScrollController(Animation.Scheduler callbackScheduler) {\n        this.lerp = new DoubleLerp(0.0, 0.0);\n        this.animation = new Animation(\n            Easing.LINEAR,\n            Duration.ofNanos(1),\n            callbackScheduler,\n            progress -> this.setOffset(this.lerp.compute(progress)),\n            Animation.Target.END\n        );\n    }\n\n    protected double offset = 0;\n    protected double maxOffset = 0;\n\n    public void animateTo(double offset, Duration duration, Easing easing) {\n        this.animation.duration = duration;\n        this.animation.easing = easing;\n        this.lerp = new DoubleLerp(this.offset, this.clampOffset(offset));\n\n        this.animation.towards(Animation.Target.END);\n    }\n\n    public void animateBy(double by, Duration duration, Easing easing) {\n        this.animateTo(this.lerp.end + by, duration, easing);\n    }\n\n    public void jumpTo(double offset) {\n        offset = this.clampOffset(offset);\n\n        this.animation.stop();\n        this.lerp = new DoubleLerp(offset, offset);\n\n        this.setOffset(offset);\n    }\n\n    public void jumpBy(double by) {\n        this.jumpTo(this.offset + by);\n    }\n\n    private void setOffset(double offset) {\n        if (this.offset == offset) {\n            return;\n        }\n\n        this.offset = this.clampOffset(offset);\n        this.notifyListeners();\n    }\n\n    private double clampOffset(double offset) {\n        return Mth.clamp(offset, 0, this.maxOffset);\n    }\n\n    public double offset() {\n        return this.offset;\n    }\n\n    boolean setMaxOffset(double maxOffset) {\n        if (this.maxOffset == maxOffset) {\n            return false;\n        }\n\n        this.maxOffset = maxOffset;\n        this.offset = this.clampOffset(this.offset);\n\n        return true;\n    }\n\n    boolean maxOffsetNotificationScheduled = false;\n    void sendMaxOffsetNotification() {\n        this.notifyListeners();\n        this.maxOffsetNotificationScheduled = false;\n    }\n\n    public double maxOffset() {\n        return this.maxOffset;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/Scrollable.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.core.Aabb2d;\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.CompoundListenable;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Clip;\nimport io.wispforest.owo.braid.widgets.basic.ListenableBuilder;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\n\nimport java.util.Objects;\n\npublic class Scrollable extends StatefulWidget {\n\n    public final boolean horizontal;\n    public final boolean vertical;\n    public final @Nullable ScrollController horizontalController;\n    public final @Nullable ScrollController verticalController;\n    public final @Nullable ScrollAnimationSettings animationSettings;\n    public final Widget child;\n\n    public Scrollable(\n        boolean horizontal,\n        boolean vertical,\n        @Nullable ScrollController horizontalController,\n        @Nullable ScrollController verticalController,\n        @Nullable ScrollAnimationSettings animationSettings,\n        Widget child\n    ) {\n        this.horizontal = horizontal;\n        this.vertical = vertical;\n        this.horizontalController = horizontalController;\n        this.verticalController = verticalController;\n        this.animationSettings = animationSettings;\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<Scrollable> createState() {\n        return new State();\n    }\n\n    // ---\n\n    public static void reveal(BuildContext context) {\n        reveal(context, Insets.none());\n    }\n\n    public static void reveal(BuildContext context, Insets padding) {\n        of(context).reveal(context, padding);\n    }\n\n    public static void revealAabb(BuildContext context, Aabb2d box) {\n        of(context).revealAabb(context, box);\n    }\n\n    public static @Nullable State maybeOf(BuildContext context) {\n        var provider = context.getAncestor(ScrollableProvider.class);\n        return provider != null ? provider.state : null;\n    }\n\n    public static State of(BuildContext context) {\n        var state = maybeOf(context);\n        Preconditions.checkNotNull(state, \"attempted to look up the enclosing scrollable state without one being present\");\n\n        return state;\n    }\n\n    // ---\n\n    public static class State extends WidgetState<Scrollable> {\n\n        protected final CompoundListenable listenable = new CompoundListenable();\n\n        protected ScrollController horizontalController;\n        protected ScrollController verticalController;\n\n        private void reveal(BuildContext context, Insets padding) {\n            var transform = context.instance().transform;\n\n            var matrix = new Matrix3x2f();\n            transform.transformToWidget(matrix);\n\n            var box = new Aabb2d(\n                transform.x() - padding.left(),\n                transform.y() - padding.top(),\n                transform.width() + padding.horizontal(),\n                transform.height() + padding.vertical()\n            ).transform(matrix);\n\n            revealAabb(context, box);\n        }\n\n        // TODO: support animations\n        private void revealAabb(BuildContext context, Aabb2d box) {\n            var scrollInstance = this.context().instance();\n            var revealInstance = context.instance();\n\n            var transform = revealInstance.computeTransformFrom(scrollInstance).invert().translate(\n                this.horizontalController != null ? (float) this.horizontalController.offset : 0,\n                this.verticalController != null ? (float) this.verticalController.offset : 0\n            );\n\n            box.transform(transform);\n\n            if (this.horizontalController != null) {\n                if (box.minX() < this.horizontalController.offset) {\n                    this.horizontalController.jumpTo(box.minX());\n                }\n\n                if (box.maxX() > scrollInstance.transform.width() + this.horizontalController.offset) {\n                    this.horizontalController.jumpTo(box.maxX() - scrollInstance.transform.width());\n                }\n            }\n\n            if (this.verticalController != null) {\n                if (box.minY() < this.verticalController.offset) {\n                    this.verticalController.jumpTo(box.minY());\n                }\n\n                if (box.maxY() > scrollInstance.transform.height() + this.verticalController.offset) {\n                    this.verticalController.jumpTo(box.maxY() - scrollInstance.transform.height());\n                }\n            }\n        }\n\n        @Override\n        public void init() {\n            this.horizontalController = this.widget().horizontal ? Objects.requireNonNullElse(this.widget().horizontalController, new ScrollController(this)) : null;\n            this.verticalController = this.widget().vertical ? Objects.requireNonNullElse(this.widget().verticalController, new ScrollController(this)) : null;\n\n            if (this.horizontalController != null) this.listenable.addChild(this.horizontalController);\n            if (this.verticalController != null) this.listenable.addChild(this.verticalController);\n        }\n\n        @Override\n        public void didUpdateWidget(Scrollable oldWidget) {\n            this.listenable.clear();\n\n            if (this.widget().horizontal) {\n                if (this.widget().horizontalController != null) {\n                    this.horizontalController = this.widget().horizontalController;\n                } else if (this.horizontalController == null || this.horizontalController == oldWidget.horizontalController) {\n                    this.horizontalController = new ScrollController(this);\n                }\n\n                this.listenable.addChild(this.horizontalController);\n            } else {\n                this.horizontalController = null;\n            }\n\n            if (this.widget().vertical) {\n                if (this.widget().verticalController != null) {\n                    this.verticalController = this.widget().verticalController;\n                } else if (this.verticalController == null || this.verticalController == oldWidget.verticalController) {\n                    this.verticalController = new ScrollController(this);\n                }\n\n                this.listenable.addChild(this.verticalController);\n            } else {\n                this.verticalController = null;\n            }\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widgetSettings = this.widget().animationSettings;\n            var animationSettings = widgetSettings != null\n                ? (widgetSettings != ScrollAnimationSettings.NO_ANIMATION ? widgetSettings : null)\n                : DefaultScrollAnimationSettings.maybeOf(context);\n\n            return new Clip(\n                new MouseArea(\n                    widget -> widget\n                        .scrollCallback((horizontal, vertical) -> {\n                            var verticalDelta = vertical * -15;\n                            var horizontalDelta = horizontal * -15;\n\n                            if (AppState.of(context).eventBinding.activeModifiers().shift()) {\n                                if (this.widget().horizontal) {\n                                    if (animationSettings != null) {\n                                        this.horizontalController.animateBy(verticalDelta, animationSettings.duration(), animationSettings.easing());\n                                    } else {\n                                        this.horizontalController.jumpBy(verticalDelta);\n                                    }\n                                }\n                            } else {\n                                if (this.widget().vertical) {\n                                    if (animationSettings != null) {\n                                        this.verticalController.animateBy(verticalDelta, animationSettings.duration(), animationSettings.easing());\n                                    } else {\n                                        this.verticalController.jumpBy(verticalDelta);\n                                    }\n                                }\n                            }\n\n                            if (this.widget().horizontal) {\n                                if (animationSettings != null) {\n                                    this.horizontalController.animateBy(horizontalDelta, animationSettings.duration(), animationSettings.easing());\n                                } else {\n                                    this.horizontalController.jumpBy(horizontalDelta);\n                                }\n                            }\n                            return true;\n                        }),\n                    new ListenableBuilder(\n                        this.listenable,\n                        (innerContext, child) -> new RawScrollView(\n                            this.horizontalController,\n                            this.verticalController,\n                            new ScrollableProvider(this, child)\n                        ),\n                        this.widget().child\n                    )\n                )\n            );\n        }\n    }\n}\n\nclass ScrollableProvider extends InheritedWidget {\n\n    public final Scrollable.State state;\n\n    public ScrollableProvider(Scrollable.State state, Widget child) {\n        super(child);\n        this.state = state;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/ScrollableWithBars.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Align;\nimport io.wispforest.owo.braid.widgets.basic.ListenableBuilder;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.function.BiFunction;\n\npublic class ScrollableWithBars extends StatefulWidget {\n\n    public final @Nullable ScrollController horizontalController;\n    public final @Nullable ScrollController verticalController;\n    public final @Nullable ScrollAnimationSettings animationSettings;\n    public final int scrollbarSize;\n    public final BiFunction<LayoutAxis, ScrollController, Scrollbar> scrollbarFactory;\n    public final Widget child;\n\n    public ScrollableWithBars(@Nullable ScrollController horizontalController, @Nullable ScrollController verticalController, @Nullable ScrollAnimationSettings animationSettings, int scrollbarSize, BiFunction<LayoutAxis, ScrollController, Scrollbar> scrollbarFactory, Widget child) {\n        this.horizontalController = horizontalController;\n        this.verticalController = verticalController;\n        this.animationSettings = animationSettings;\n        this.scrollbarSize = scrollbarSize;\n        this.scrollbarFactory = scrollbarFactory;\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<ScrollableWithBars> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<ScrollableWithBars> {\n\n        private ScrollController horizontalController;\n        private ScrollController verticalController;\n\n        private void updateControllers() {\n            var newHorizontalController = this.widget().horizontalController != null ? this.widget().horizontalController : this.horizontalController;\n            this.horizontalController = newHorizontalController != null ? newHorizontalController : new ScrollController(this);\n\n            var newVerticalController = this.widget().verticalController != null ? this.widget().verticalController : this.verticalController;\n            this.verticalController = newVerticalController != null ? newVerticalController : new ScrollController(this);\n        }\n\n        @Override\n        public void init() {\n            this.updateControllers();\n        }\n\n        @Override\n        public void didUpdateWidget(ScrollableWithBars oldWidget) {\n            this.updateControllers();\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new ListenableBuilder(\n                this.horizontalController,\n                horizontalContext -> {\n                    var showHorizontalScrollbar = this.horizontalController.maxOffset() > 0;\n\n                    return new ListenableBuilder(\n                        this.verticalController,\n                        verticalContext -> {\n                            var showVerticalScrollbar = this.verticalController.maxOffset() > 0;\n\n                            var widgets = new ArrayList<Widget>();\n                            widgets.add(new StackBase(\n                                new Padding(\n                                    Insets.of(\n                                        0,\n                                        showHorizontalScrollbar ? this.widget().scrollbarSize : 0,\n                                        0,\n                                        showVerticalScrollbar ? this.widget().scrollbarSize : 0\n                                    ),\n                                    new Scrollable(\n                                        true,\n                                        true,\n                                        this.horizontalController,\n                                        this.verticalController,\n                                        this.widget().animationSettings,\n                                        this.widget().child\n                                    )\n                                )\n                            ));\n\n                            if (showVerticalScrollbar) {\n                                widgets.add(new Align(\n                                    Alignment.RIGHT,\n                                    new Padding(\n                                        showHorizontalScrollbar ? Insets.bottom(this.widget().scrollbarSize) : Insets.none(),\n                                        new Sized(\n                                            this.widget().scrollbarSize,\n                                            null,\n                                            this.widget().scrollbarFactory.apply(LayoutAxis.VERTICAL, this.verticalController)\n                                        )\n                                    )\n                                ));\n                            }\n\n                            if (showHorizontalScrollbar) {\n                                widgets.add(new Align(\n                                    Alignment.BOTTOM,\n                                    new Sized(\n                                        null,\n                                        this.widget().scrollbarSize,\n                                        this.widget().scrollbarFactory.apply(LayoutAxis.HORIZONTAL, this.horizontalController)\n                                    )\n                                ));\n                            }\n\n                            return new Stack(widgets);\n                        }\n                    );\n                }\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/Scrollbar.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.LayoutBuilder;\nimport io.wispforest.owo.braid.widgets.basic.ListenableBuilder;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.slider.SliderStyle;\nimport io.wispforest.owo.braid.widgets.slider.slider.Slider;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Scrollbar extends StatelessWidget {\n\n    public final LayoutAxis axis;\n    public final ScrollController controller;\n    public final @Nullable Widget track;\n    public final Widget handle;\n\n    public Scrollbar(LayoutAxis axis, ScrollController controller, @Nullable Widget track, Widget handle) {\n        this.axis = axis;\n        this.controller = controller;\n        this.track = track;\n        this.handle = handle;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new ListenableBuilder(\n            this.controller,\n            buildContext -> {\n                return new LayoutBuilder(\n                    (ctx, constraints) -> {\n                        var currentOffset = this.controller.offset;\n                        var maxOffset = this.controller.maxOffset;\n\n                        var containerSize = constraints.maxOnAxis(this.axis);\n                        var childSize = containerSize + maxOffset;\n                        var scrollbarLength = Math.floor(Math.min((containerSize / childSize) * containerSize, containerSize));\n\n                        return maxOffset != 0 ? new Slider(\n                            currentOffset,\n                            widget -> widget\n                                .style(new SliderStyle<>(\n                                    this.track,\n                                    active -> this.handle,\n                                    Math.max(5, scrollbarLength),\n                                    null\n                                ))\n                                .min(this.axis.choose(0d, maxOffset))\n                                .max(this.axis.choose(maxOffset, 0d))\n                                .axis(this.axis),\n                            this.controller::jumpTo\n                        ) : new Padding(Insets.none());\n                    }\n                );\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/scroll/VerticallyScrollable.java",
    "content": "package io.wispforest.owo.braid.widgets.scroll;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\npublic class VerticallyScrollable extends Scrollable {\n    public VerticallyScrollable(@Nullable ScrollController controller, @Nullable ScrollAnimationSettings animationSettings, Widget child) {\n        super(false, true, null, controller, animationSettings, child);\n    }\n\n    public VerticallyScrollable(Widget child) {\n        this(null, null, child);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/sharedstate/ShareableState.java",
    "content": "package io.wispforest.owo.braid.widgets.sharedstate;\n\npublic abstract class ShareableState {\n    SharedState.State<?> backingState;\n\n    public final void setState(Runnable fn) {\n        this.backingState.setState(() -> {\n            fn.run();\n            this.backingState.generation++;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/sharedstate/SharedState.java",
    "content": "package io.wispforest.owo.braid.widgets.sharedstate;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\npublic class SharedState<T extends ShareableState> extends StatefulWidget {\n    public final Supplier<T> initState;\n    public final Widget child;\n\n    public SharedState(Supplier<T> initState, Widget child) {\n        this.initState = initState;\n        this.child = child;\n    }\n\n    @Override\n    public WidgetState<SharedState<T>> createState() {\n        return new State<>();\n    }\n\n    public static <T extends ShareableState> T get(BuildContext context, Class<T> clazz) {\n        var provider = context.dependOnAncestor(SharedStateProvider.class, SharedStateProvider.keyOf(clazz));\n        Preconditions.checkArgument(provider != null, \"attempted to read shared state which is not provided by the current context\");\n\n        return (T) provider.state.state;\n    }\n\n    public static <T extends ShareableState> T getWithoutDependency(BuildContext context, Class<T> clazz) {\n        var provider = context.getAncestor(SharedStateProvider.class, SharedStateProvider.keyOf(clazz));\n        Preconditions.checkArgument(provider != null, \"attempted to read shared state which is not provided by the current context\");\n\n        return (T) provider.state.state;\n    }\n\n    public static <T extends ShareableState, S> S select(BuildContext context, Class<T> clazz, Function<T, S> selector) {\n        var provider = context.getAncestor(SharedStateProvider.class, SharedStateProvider.keyOf(clazz));\n        Preconditions.checkArgument(provider != null, \"attempted to select from shared state which is not provided by the current context\");\n\n        var capturedValue = selector.apply(((SharedStateProvider<T>) provider).state.state);\n        context.dependOnAncestor(SharedStateProvider.class, SharedStateProvider.keyOf(clazz), SharedStateProvider.dependencyOf(clazz, capturedValue, selector));\n\n        return capturedValue;\n    }\n\n    public static <T extends ShareableState> void set(BuildContext context, Class<T> clazz, Consumer<T> consumer) {\n        var provider = context.dependOnAncestor(SharedStateProvider.class, SharedStateProvider.keyOf(clazz));\n        Preconditions.checkArgument(provider != null, \"attempted to set shared state which is not provided by the current context\");\n\n        provider.state.state.setState(() -> consumer.accept((T) provider.state.state));\n    }\n\n    public static class State<T extends ShareableState> extends WidgetState<SharedState<T>> {\n        public T state;\n        public int generation = 0;\n\n        @Override\n        public void init() {\n            super.init();\n\n            this.state = widget().initState.get();\n            this.state.backingState = this;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new SharedStateProvider<>(this, this.generation, this.widget().child);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/sharedstate/SharedStateProvider.java",
    "content": "package io.wispforest.owo.braid.widgets.sharedstate;\n\nimport com.google.common.collect.Iterables;\nimport io.wispforest.owo.braid.framework.proxy.InheritedProxy;\nimport io.wispforest.owo.braid.framework.proxy.WidgetProxy;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.*;\nimport java.util.function.Function;\n\npublic final class SharedStateProvider<T extends ShareableState> extends InheritedWidget {\n    public final SharedState.State<T> state;\n    public final int generation;\n\n    private final InheritedKey inheritedKey;\n\n    public SharedStateProvider(SharedState.State<T> state, int generation, Widget child) {\n        super(child);\n        this.state = state;\n        this.generation = generation;\n\n        this.inheritedKey = new InheritedKey(state.state.getClass());\n    }\n\n    @Override\n    public WidgetProxy proxy() {\n        return new Proxy<>(this);\n    }\n\n    @Override\n    public Object inheritedKey() {\n        return this.inheritedKey;\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return generation != ((SharedStateProvider<?>) newWidget).generation;\n    }\n\n    public static Object keyOf(Class<? extends ShareableState> stateClass) {\n        return new InheritedKey(stateClass);\n    }\n\n    public static <T> Object dependencyOf(Class<T> stateClass, @Nullable Object capturedValue, Function<T, ? extends @Nullable Object> selector) {\n        return new StateAspect<>(stateClass, capturedValue, selector);\n    }\n\n    public static class Proxy<T extends ShareableState> extends InheritedProxy {\n\n        private static final Object COMPLETE_DEPENDENCY_SENTINEL = new Object();\n        private final Map<WidgetProxy, Object> dependenciesByDependent = new HashMap<>();\n\n        public Proxy(SharedStateProvider<T> widget) {\n            super(widget);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        @Override\n        public void addDependency(WidgetProxy dependent, @Nullable Object dependency) {\n            super.addDependency(dependent, dependency);\n\n            var existingDependency = this.dependenciesByDependent.get(dependent);\n            if (existingDependency != null && !(existingDependency instanceof List<?>)) {\n                return;\n            }\n\n            if (!(dependency instanceof StateAspect<?> aspect) || aspect.stateClass() != ((SharedStateProvider<T>) this.widget()).state.state.getClass()) {\n                this.dependenciesByDependent.put(dependent, COMPLETE_DEPENDENCY_SENTINEL);\n                return;\n            }\n\n            List<StateAspect<T>> aspects;\n            if (existingDependency != null) {\n                aspects = (List<StateAspect<T>>) existingDependency;\n            } else {\n                aspects = new ArrayList<>();\n                this.dependenciesByDependent.put(dependent, aspects);\n            }\n\n            aspects.add((StateAspect<T>) dependency);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        @Override\n        protected boolean mustRebuildDependent(WidgetProxy dependent) {\n            var dependency = this.dependenciesByDependent.get(dependent);\n            if (dependency instanceof List<?>) {\n                return Iterables.any(\n                    (List<StateAspect<T>>) dependency,\n                    element -> !Objects.equals(element.capturedValue(), element.selector().apply(((SharedStateProvider<T>) this.widget()).state.state))\n                );\n            } else {\n                return true;\n            }\n        }\n\n        @Override\n        public void notifyDependent(WidgetProxy dependent) {\n            super.notifyDependent(dependent);\n            this.dependenciesByDependent.remove(dependent);\n        }\n    }\n}\n\nrecord InheritedKey(Class<?> stateClass) {}\n\nrecord StateAspect<T>(Class<T> stateClass, @Nullable Object capturedValue, Function<T, ? extends @Nullable Object> selector) {}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/DefaultSliderHandle.java",
    "content": "package io.wispforest.owo.braid.widgets.slider;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.EmptyWidget;\nimport io.wispforest.owo.braid.widgets.button.ButtonPanel;\n\npublic class DefaultSliderHandle extends StatelessWidget {\n\n    public final boolean active;\n\n    public DefaultSliderHandle(boolean active) {\n        this.active = active;\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new ButtonPanel(\n            this.active,\n            EmptyWidget.INSTANCE\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/Incrementor.java",
    "content": "package io.wispforest.owo.braid.widgets.slider;\n\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.intents.Intent;\nimport io.wispforest.owo.braid.widgets.intents.Interactable;\nimport io.wispforest.owo.braid.widgets.intents.ShortcutTrigger;\nimport net.minecraft.util.Util;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.DoubleConsumer;\n\npublic class Incrementor extends StatelessWidget {\n\n    public final @Nullable DoubleConsumer xCallback, yCallback;\n    public final Widget child;\n\n    public Incrementor(\n        @Nullable DoubleConsumer xCallback,\n        @Nullable DoubleConsumer yCallback,\n        Widget child\n    ) {\n        this.xCallback = xCallback;\n        this.yCallback = yCallback;\n        this.child = child;\n    }\n\n    public Incrementor(@Nullable DoubleConsumer callback, Widget child) {\n        this(callback, callback, child);\n    }\n\n    public Incrementor(LayoutAxis axis, @Nullable DoubleConsumer callback, Widget child) {\n        this(\n            axis.choose(callback, null),\n            axis.choose(null, callback),\n            child\n        );\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Interactable(\n            this.xCallback != null && this.yCallback != null ? BOTH_AXIS_SHORTCUTS : this.xCallback != null ? HORIZONTAL_SHORTCUTS : this.yCallback != null ? VERTICAL_SHORTCUTS : Map.of(),\n            interactable -> interactable.addCallbackAction(\n                IncrementIntent.class,\n                (actionCtx, intent) -> {\n                    switch (intent.axis) {\n                        case HORIZONTAL -> {\n                            if (this.xCallback != null) this.xCallback.accept(intent.amount);\n                        }\n                        case VERTICAL -> {\n                            if (this.yCallback != null) this.yCallback.accept(intent.amount);\n                        }\n                    }\n                }\n            ),\n            new MouseArea(\n                mouseArea -> mouseArea\n                    .scrollCallback((baseHorizontal, baseVertical) -> {\n                        var handled = false;\n                        var modifiers = AppState.of(context).eventBinding.activeModifiers();\n                        var horizontal = modifiers.shift() ? baseVertical : baseHorizontal;\n                        var vertical = modifiers.shift() ? baseHorizontal : baseVertical;\n\n                        if (horizontal != 0 && this.xCallback != null) {\n                            this.xCallback.accept(horizontal);\n                            handled = true;\n                        }\n                        if (vertical != 0 && this.yCallback != null) {\n                            this.yCallback.accept(vertical);\n                            handled = true;\n                        }\n                        return handled;\n                    }),\n                this.child\n            )\n        );\n    }\n\n    // ---\n\n    public static final Map<List<ShortcutTrigger>, Intent> HORIZONTAL_SHORTCUTS = Map.of(\n        List.of(ShortcutTrigger.RIGHT), new IncrementIntent(LayoutAxis.HORIZONTAL, 1),\n        List.of(ShortcutTrigger.LEFT), new IncrementIntent(LayoutAxis.HORIZONTAL, -1),\n        List.of(ShortcutTrigger.HOME), new IncrementIntent(LayoutAxis.HORIZONTAL, Double.NEGATIVE_INFINITY),\n        List.of(ShortcutTrigger.END), new IncrementIntent(LayoutAxis.HORIZONTAL, Double.POSITIVE_INFINITY)\n    );\n\n    public static final Map<List<ShortcutTrigger>, Intent> VERTICAL_SHORTCUTS = Map.of(\n        List.of(ShortcutTrigger.UP), new IncrementIntent(LayoutAxis.VERTICAL, 1),\n        List.of(ShortcutTrigger.DOWN), new IncrementIntent(LayoutAxis.VERTICAL, -1),\n        List.of(ShortcutTrigger.PAGE_UP), new IncrementIntent(LayoutAxis.VERTICAL, Double.POSITIVE_INFINITY),\n        List.of(ShortcutTrigger.PAGE_DOWN), new IncrementIntent(LayoutAxis.VERTICAL, Double.NEGATIVE_INFINITY)\n    );\n\n    public static final Map<List<ShortcutTrigger>, Intent> BOTH_AXIS_SHORTCUTS = Util.make(() -> {\n        var map = new HashMap<List<ShortcutTrigger>, Intent>();\n        map.putAll(HORIZONTAL_SHORTCUTS);\n        map.putAll(VERTICAL_SHORTCUTS);\n        return Collections.unmodifiableMap(map);\n    });\n\n    public record IncrementIntent(LayoutAxis axis, double amount) implements Intent {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/SliderStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.slider;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.sounds.SoundEvent;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Optional;\n\npublic record SliderStyle<HandleSize>(\n    @Nullable Widget track,\n    @Nullable HandleBuilder handleBuilder,\n    @Nullable HandleSize handleSize,\n    @Nullable Optional<SoundEvent> confirmSound\n) {\n    public SliderStyle<HandleSize> overriding(SliderStyle<HandleSize> other) {\n        //noinspection OptionalAssignedToNull\n        return new SliderStyle<>(\n            this.track != null ? this.track : other.track,\n            this.handleBuilder != null ? this.handleBuilder : other.handleBuilder,\n            this.handleSize != null ? this.handleSize : other.handleSize,\n            this.confirmSound != null ? this.confirmSound : other.confirmSound\n        );\n    }\n\n    private static final SliderStyle<?> DEFAULT = new SliderStyle<>(null, null, null, null);\n    public static <HandleSize> SliderStyle<HandleSize> getDefault() {\n        //noinspection unchecked\n        return (SliderStyle<HandleSize>) DEFAULT;\n    }\n\n    @FunctionalInterface\n    public interface HandleBuilder {\n        Widget build(boolean active);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/drag/Drag.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.drag;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.owo.braid.widgets.slider.slider.SliderCallback;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport org.jetbrains.annotations.Nullable;\n\npublic class Drag extends RawDrag {\n\n    public Drag(\n        double value,\n        @Nullable WidgetSetupCallback<Drag> setupCallback,\n        @Nullable SliderCallback onChanged\n    ) {\n        super(\n            value,\n            null,\n            onChanged,\n            new Panel(ButtonComponent.DISABLED_TEXTURE)\n        );\n        if (setupCallback != null) setupCallback.setup(this);\n    }\n\n    public Drag(\n        double value,\n        @Nullable WidgetSetupCallback<Drag> setupCallback,\n        SliderCallback onChanged,\n        boolean active\n    ) {\n        this(value, setupCallback, active ? onChanged : null);\n    }\n\n    @Override\n    public Drag min(@Nullable Double min) {\n        return (Drag) super.min(min);\n    }\n\n    @Override\n    public Drag min(double min) {\n        return (Drag) super.min(min);\n    }\n\n    @Override\n    public Drag max(@Nullable Double max) {\n        return (Drag) super.max(max);\n    }\n\n    @Override\n    public Drag max(double max) {\n        return (Drag) super.max(max);\n    }\n\n    @Override\n    public Drag range(@Nullable Double min, @Nullable Double max) {\n        return (Drag) super.range(min, max);\n    }\n\n    @Override\n    public Drag range(double min, double max) {\n        return (Drag) super.range(min, max);\n    }\n\n    @Override\n    public Drag step(@Nullable Double step) {\n        return (Drag) super.step(step);\n    }\n\n    @Override\n    public Drag step(double step) {\n        return (Drag) super.step(step);\n    }\n\n    @Override\n    public Drag dragFunction(DragFunction dragFunction) {\n        return (Drag) super.dragFunction(dragFunction);\n    }\n\n    @Override\n    public Drag axis(LayoutAxis axis) {\n        return (Drag) super.axis(axis);\n    }\n\n    @Override\n    public Drag vertical() {\n        return (Drag) super.vertical();\n    }\n\n    @Override\n    public Drag wrap(boolean wrap) {\n        return (Drag) super.wrap(wrap);\n    }\n\n    @Override\n    public Drag dragMultiplier(double dragMultiplier) {\n        return (Drag) super.dragMultiplier(dragMultiplier);\n    }\n\n    @Override\n    public Drag incrementStep(double incrementStep) {\n        return (Drag) super.incrementStep(incrementStep);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/drag/DragFunction.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.drag;\n\nimport org.jetbrains.annotations.Nullable;\n\npublic interface DragFunction {\n\n    double deltaValue(double currentValue, @Nullable Double min, @Nullable Double max, double cursorNormalizedDelta);\n\n    DragFunction LINEAR = (currentValue, min, max, cursorDelta) -> {\n        if (min != null && max != null) return cursorDelta * (max - min);\n        return cursorDelta;\n    };\n\n    DragFunction LOGARITHMIC = (currentValue, min, max, cursorDelta) -> {\n        double base;\n        if (min != null && max != null) {\n            base = cursorDelta * (max - min);\n            double denom = Math.max(Math.abs(min), Math.abs(max));\n            double rel = denom > 0 ? Math.abs(currentValue) / denom : 0;\n            double scale = 1.0 + rel;\n            return base * scale;\n        } else {\n            double scale = 1.0 + Math.min(2.0, Math.log1p(Math.abs(currentValue)));\n            return cursorDelta * scale;\n        }\n    };\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/drag/MessageDrag.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.drag;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.slider.slider.SliderCallback;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\npublic class MessageDrag extends StatelessWidget {\n\n    public final double value;\n    public final @Nullable WidgetSetupCallback<Drag> setupCallback;\n    public final @Nullable SliderCallback onChanged;\n\n    public final Component message;\n\n    public MessageDrag(\n        double value,\n        @Nullable WidgetSetupCallback<Drag> setupCallback,\n        @Nullable SliderCallback onChanged,\n        Component message\n    ) {\n        this.value = value;\n        this.setupCallback = setupCallback;\n        this.onChanged = onChanged;\n        this.message = message;\n    }\n\n    public MessageDrag(\n        double value,\n        @Nullable WidgetSetupCallback<Drag> setupCallback,\n        boolean active,\n        SliderCallback onChanged,\n        Component message\n    ) {\n        this(value, setupCallback, active ? onChanged : null, message);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Stack(\n            new Drag(\n                this.value,\n                this.setupCallback,\n                this.onChanged\n            ),\n            new Label(\n                LabelStyle.SHADOW,\n                false,\n                this.message\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/drag/RawDrag.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.drag;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.slider.Incrementor;\nimport io.wispforest.owo.braid.widgets.slider.slider.SliderCallback;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\n\npublic class RawDrag extends StatefulWidget {\n\n    public final double value;\n    protected @Nullable Double min = 0d;\n    protected @Nullable Double max = 1d;\n    protected @Nullable Double step;\n    protected DragFunction dragFunction = DragFunction.LINEAR;\n    protected double dragMultiplier = 1;\n    protected LayoutAxis axis = LayoutAxis.HORIZONTAL;\n    protected boolean wrap = false;\n\n    public final @Nullable SliderCallback onChanged;\n    public final @Nullable Widget child;\n\n    protected @Nullable Double incrementStep = null;\n\n    public RawDrag(\n        double value,\n        @Nullable WidgetSetupCallback<RawDrag> setupCallback,\n        @Nullable SliderCallback onChanged,\n        @Nullable Widget child\n    ) {\n        this.value = value;\n        this.onChanged = onChanged;\n        this.child = child;\n        if (setupCallback != null) setupCallback.setup(this);\n    }\n\n    public RawDrag min(@Nullable Double min) {\n        this.assertMutable();\n        this.min = min;\n        return this;\n    }\n\n    public RawDrag min(double min) {\n        this.assertMutable();\n        this.min = min;\n        return this;\n    }\n\n    public @Nullable Double min() {\n        return this.min;\n    }\n\n    public RawDrag max(@Nullable Double max) {\n        this.assertMutable();\n        this.max = max;\n        return this;\n    }\n\n    public RawDrag max(double max) {\n        this.assertMutable();\n        this.max = max;\n        return this;\n    }\n\n    public @Nullable Double max() {\n        return this.max;\n    }\n\n    public RawDrag range(@Nullable Double min, @Nullable Double max) {\n        this.assertMutable();\n        this.min = min;\n        this.max = max;\n        return this;\n    }\n\n    public RawDrag range(double min, double max) {\n        this.assertMutable();\n        this.min = min;\n        this.max = max;\n        return this;\n    }\n\n    public RawDrag step(@Nullable Double step) {\n        this.assertMutable();\n        this.step = step;\n        return this;\n    }\n\n    public RawDrag step(double step) {\n        this.assertMutable();\n        this.step = step;\n        return this;\n    }\n\n    public @Nullable Double step() {\n        return this.step;\n    }\n\n    public RawDrag dragFunction(DragFunction dragFunction) {\n        this.assertMutable();\n        this.dragFunction = dragFunction;\n        return this;\n    }\n\n    public DragFunction dragFunction() {\n        return this.dragFunction;\n    }\n\n    public RawDrag axis(LayoutAxis axis) {\n        this.assertMutable();\n        this.axis = axis;\n        return this;\n    }\n\n    public RawDrag vertical() {\n        return this.axis(LayoutAxis.VERTICAL);\n    }\n\n    public LayoutAxis axis() {\n        return this.axis;\n    }\n\n    public RawDrag wrap(boolean wrap) {\n        this.assertMutable();\n        this.wrap = wrap;\n        return this;\n    }\n\n    public boolean wrap() {\n        return this.wrap;\n    }\n\n    public RawDrag dragMultiplier(double dragMultiplier) {\n        this.assertMutable();\n        this.dragMultiplier = dragMultiplier;\n        return this;\n    }\n\n    public double dragMultiplier() {\n        return this.dragMultiplier;\n    }\n\n    public RawDrag incrementStep(double incrementStep) {\n        this.assertMutable();\n        this.incrementStep = incrementStep;\n        return this;\n    }\n\n    public @Nullable Double incrementStep() {\n        return this.incrementStep;\n    }\n\n\n    @Override\n    public WidgetState<RawDrag> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<RawDrag> {\n\n        protected double dragValue = 0;\n        protected boolean dragging = false;\n\n        protected double normalizedValue;\n        protected double incrementStep;\n        protected CursorStyle draggingCursorStyle = null;\n\n        @Override\n        public void init() {\n            var widget = this.widget();\n            // incrementStep in drag: when bounded, treat increment as fraction of range; when unbounded, as raw value units\n            if (widget.min != null && widget.max != null) {\n                var range = widget.max - widget.min;\n                var inc = widget.incrementStep != null ? widget.incrementStep : (widget.step != null ? widget.step : range * 0.01);\n                this.incrementStep = inc / (range == 0 ? 1 : range);\n            } else {\n                this.incrementStep = widget.incrementStep != null ? widget.incrementStep : (widget.step != null ? widget.step : 1.0);\n            }\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            this.normalizedValue = (widget.min != null && widget.max != null)? (widget.value - widget.min) / (widget.max - widget.min) : 0;\n            this.draggingCursorStyle = null;\n            return new LayoutBuilder((innerContext, constraints) -> {\n                var size = constraints.maxFiniteOrMinSize();\n                var content = new Sized(size, widget.child);\n                return new Center(\n                    widget.onChanged == null || ControlsOverride.controlsDisabled(context)\n                        ? content\n                        : new Incrementor(\n                            widget.axis,\n                            increment -> this.increment(constraints, increment),\n                            new MouseArea(\n                                mouseArea -> mouseArea\n                                    .clickCallback((x, y, button, modifiers) -> {\n                                        if (button != 0) return false;\n                                        this.dragValue = this.normalizedValue;\n                                        this.dragging = true;\n                                        return true;\n                                    })\n                                    .dragCallback((x, y, dx, dy) -> {\n                                        var delta = widget.axis.choose(dx, widget.axis == LayoutAxis.VERTICAL ? -dy : dy);\n                                        this.move(constraints, delta);\n                                    })\n                                    .dragEndCallback(() -> dragging = false)\n                                    .cursorStyleSupplier((x, y) -> {\n                                        if (dragging) {\n                                            if (draggingCursorStyle == null) this.draggingCursorStyle = CursorStyle.forDraggingAlong(widget.axis, context.instance().computeGlobalTransform());\n                                            return this.draggingCursorStyle;\n                                        }\n                                        return CursorStyle.HAND;\n                                    }),\n                                content\n                            )\n                        )\n                );\n            });\n        }\n\n        protected void move(Constraints constraints, double deltaAlongAxis) {\n            var widget = this.widget();\n            if (widget.min != null && widget.max != null) {\n                var track = Math.max(1, constraints.maxFiniteOrMinOnAxis(widget.axis));\n                var cursorNorm = (deltaAlongAxis / track) * widget.dragMultiplier;\n                var valueDelta = widget.dragFunction.deltaValue(widget.value, widget.min, widget.max, cursorNorm);\n                var newValue = widget.value + valueDelta;\n                this.applyValueBounded(newValue);\n            } else {\n                var track = Math.max(1, constraints.maxFiniteOrMinOnAxis(widget.axis));\n                var cursorNorm = (deltaAlongAxis / track) * widget.dragMultiplier;\n                var valueDelta = widget.dragFunction.deltaValue(widget.value, null, null, cursorNorm);\n                this.applyValueUnbounded(widget.value + valueDelta);\n            }\n        }\n\n        protected void increment(Constraints constraints, double increment) {\n            var widget = this.widget();\n            if (widget.min != null && widget.max != null) {\n                var range = widget.max - widget.min;\n                var valueDelta = incrementStep * increment * range;\n                this.applyValueBounded(widget.value + valueDelta);\n            } else {\n                var unit = widget.incrementStep != null ? widget.incrementStep : (widget.step != null ? widget.step : 1.0);\n                this.applyValueUnbounded(widget.value + unit * increment);\n            }\n        }\n\n        protected void applyValueBounded(double newValue) {\n            var widget = this.widget();\n            var min = widget.min == null ? Double.NEGATIVE_INFINITY : widget.min;\n            var max = widget.max == null ? Double.POSITIVE_INFINITY : widget.max;\n            if (widget.wrap && widget.min != null && widget.max != null) {\n                var range = max - min;\n                if (range != 0) {\n                    var offset = (newValue - min) % range;\n                    if (offset < 0) offset += range;\n                    newValue = min + offset;\n                }\n            } else {\n                newValue = Mth.clamp(newValue, min, max);\n            }\n            var step = widget.step;\n            newValue = step != null ? Math.round(newValue / step) * step : newValue;\n            if (widget.wrap && widget.min != null && widget.max != null) {\n                var range = max - min;\n                if (range != 0) {\n                    var offset = (newValue - min) % range;\n                    if (offset < 0) offset += range;\n                    newValue = min + offset;\n                }\n            } else {\n                newValue = Mth.clamp(newValue, min, max);\n            }\n            widget.onChanged.accept(newValue);\n        }\n\n        protected void applyValueUnbounded(double newValue) {\n            var widget = this.widget();\n            var step = widget.step;\n            widget.onChanged.accept(step != null ? Math.round(newValue / step) * step : newValue);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/range/DefaultRangeSliderStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.range;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport org.jetbrains.annotations.Nullable;\n\npublic class DefaultRangeSliderStyle extends InheritedWidget {\n\n    public final RangeSliderStyle style;\n\n    public DefaultRangeSliderStyle(RangeSliderStyle style, Widget child) {\n        super(child);\n        this.style = style;\n    }\n\n    public static Widget merge(RangeSliderStyle style, Widget child) {\n        return new Builder(context -> {\n            var contextStyle = DefaultRangeSliderStyle.maybeOf(context);\n            return new DefaultRangeSliderStyle(contextStyle != null ? style.overriding(contextStyle) : style, child);\n        });\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return !this.style.equals(((DefaultRangeSliderStyle) newWidget).style);\n    }\n\n    public static @Nullable RangeSliderStyle maybeOf(BuildContext context) {\n        var widget = context.dependOnAncestor(DefaultRangeSliderStyle.class);\n        if (widget != null) {\n            return widget.style;\n        } else {\n            return null;\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/range/MessageRangeSlider.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.range;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\npublic class MessageRangeSlider extends StatelessWidget {\n\n    public final double minValue, maxValue;\n    public final @Nullable WidgetSetupCallback<RangeSlider> setupCallback;\n    public final @Nullable RangeSliderCallback onChanged;\n    public final Component message;\n\n    public MessageRangeSlider(\n        double minValue,\n        double maxValue,\n        Component message,\n        @Nullable WidgetSetupCallback<RangeSlider> setupCallback,\n        @Nullable RangeSliderCallback onChanged\n    ) {\n        this.minValue = minValue;\n        this.maxValue = maxValue;\n        this.setupCallback = setupCallback;\n        this.onChanged = onChanged;\n        this.message = message;\n    }\n\n    public MessageRangeSlider(\n        double minValue,\n        double maxValue,\n        Component message,\n        @Nullable WidgetSetupCallback<RangeSlider> setupCallback,\n        boolean active,\n        RangeSliderCallback onChanged\n    ) {\n        this(minValue, maxValue, message, setupCallback, active ? onChanged : null);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Stack(\n            new RangeSlider(\n                this.minValue,\n                this.maxValue,\n                this.setupCallback,\n                this.onChanged\n            ),\n            new Label(\n                LabelStyle.SHADOW,\n                false,\n                this.message\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/range/RangeSlider.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.range;\n\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.slider.DefaultSliderHandle;\nimport io.wispforest.owo.braid.widgets.slider.Incrementor;\nimport io.wispforest.owo.braid.widgets.slider.slider.SliderFunction;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.util.UISounds;\nimport net.minecraft.sounds.SoundEvents;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\n\npublic class RangeSlider extends StatefulWidget {\n\n    protected double min = 0;\n    protected double max = 1;\n    protected double minRange = 0;\n    protected double maxRange = -1;\n    protected @Nullable Double step;\n    protected SliderFunction sliderFunction = SliderFunction.LINEAR;\n    protected LayoutAxis axis = LayoutAxis.HORIZONTAL;\n    protected @Nullable Double incrementStep = null;\n    protected @Nullable RangeSliderStyle style = null;\n\n    public final double minValue, maxValue;\n    public final @Nullable RangeSliderCallback onChanged;\n\n    public RangeSlider(\n        double minValue,\n        double maxValue,\n        @Nullable WidgetSetupCallback<RangeSlider> setupCallback,\n        @Nullable RangeSliderCallback onChanged\n    ) {\n        this.minValue = minValue;\n        this.maxValue = maxValue;\n        this.onChanged = onChanged;\n        if (setupCallback != null) setupCallback.setup(this);\n    }\n\n    public RangeSlider(\n        double minValue,\n        double maxValue,\n        @Nullable WidgetSetupCallback<RangeSlider> setupCallback,\n        boolean active,\n        RangeSliderCallback onChanged\n    ) {\n        this(\n            minValue, maxValue,\n            setupCallback,\n            active ? onChanged : null\n        );\n    }\n\n    public RangeSlider min(double min) {\n        this.assertMutable();\n        this.min = min;\n        return this;\n    }\n\n    public double min() {\n        return this.min;\n    }\n\n    public RangeSlider max(double max) {\n        this.assertMutable();\n        this.max = max;\n        return this;\n    }\n\n    public double max() {\n        return this.max;\n    }\n\n    public RangeSlider range(double min, double max) {\n        this.assertMutable();\n        this.min = min;\n        this.max = max;\n        return this;\n    }\n\n    public RangeSlider minRange(double minRange) {\n        this.assertMutable();\n        this.minRange = minRange;\n        return this;\n    }\n\n    public double minRange() {\n        return this.minRange;\n    }\n\n    public RangeSlider maxRange(double maxRange) {\n        this.assertMutable();\n        this.maxRange = maxRange;\n        return this;\n    }\n\n    public double maxRange() {\n        return this.maxRange;\n    }\n\n    public RangeSlider clampRange(double minRange, double maxRange) {\n        this.assertMutable();\n        this.minRange = minRange;\n        this.maxRange = maxRange;\n        return this;\n    }\n\n    public RangeSlider step(@Nullable Double step) {\n        this.assertMutable();\n        this.step = step;\n        return this;\n    }\n\n    public RangeSlider step(double step) {\n        this.assertMutable();\n        this.step = step;\n        return this;\n    }\n\n    public @Nullable Double step() {\n        return this.step;\n    }\n\n    public RangeSlider sliderFunction(SliderFunction function) {\n        this.assertMutable();\n        this.sliderFunction = function;\n        return this;\n    }\n\n    public SliderFunction sliderFunction() {\n        return this.sliderFunction;\n    }\n\n    public RangeSlider axis(LayoutAxis axis) {\n        this.assertMutable();\n        this.axis = axis;\n        return this;\n    }\n\n    public RangeSlider vertical() {\n        return this.axis(LayoutAxis.VERTICAL);\n    }\n\n    public LayoutAxis axis() {\n        return this.axis;\n    }\n\n    public RangeSlider incrementStep(double incrementStep) {\n        this.assertMutable();\n        this.incrementStep = incrementStep;\n        return this;\n    }\n\n    public @Nullable Double incrementStep() {\n        return this.incrementStep;\n    }\n\n    public RangeSlider style(RangeSliderStyle style) {\n        this.assertMutable();\n        this.style = style;\n        return this;\n    }\n\n    public @Nullable RangeSliderStyle style() {\n        return this.style;\n    }\n\n    @Override\n    public WidgetState<?> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<RangeSlider> {\n\n        protected double dragValue = 0;\n        protected @Nullable Handle grabbedHandle = null;\n        protected boolean dragging = false;\n\n        protected double normalizedMin;\n        protected double normalizedMax;\n        protected double incrementStep;\n        protected CursorStyle draggingCursorStyle = null;\n        protected double dragWidth;\n\n        protected double minHandleSize;\n        protected double maxHandleSize;\n\n        @Override\n        public void init() {\n            var widget = this.widget();\n            var trueMin = Math.min(widget.min, widget.max);\n            var trueMax = Math.max(widget.min, widget.max);\n            this.incrementStep = widget.incrementStep != null\n                ? widget.sliderFunction.normalize(widget.incrementStep, trueMin, trueMax)\n                : widget.step != null\n                    ? widget.sliderFunction.normalize(widget.step, trueMin, trueMax)\n                    : 0.01;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            var effectiveStyle = widget.style != null ? widget.style : RangeSliderStyle.DEFAULT;\n            if (DefaultRangeSliderStyle.maybeOf(context) instanceof RangeSliderStyle contextStyle) {\n                effectiveStyle = effectiveStyle.overriding(contextStyle);\n            }\n\n            var disabled = widget.onChanged == null || ControlsOverride.controlsDisabled(context);\n\n            var track = Objects.requireNonNullElse(effectiveStyle.track(), DEFAULT_TRACK);\n            var rangeIndicator = Objects.requireNonNullElse(effectiveStyle.rangeIndicator(), DEFAULT_RANGE_INDICATOR);\n            var minHandle = Objects.requireNonNullElse(effectiveStyle.minHandleBuilder(), DEFAULT_HANDLE_BUILDER).build(!disabled);\n            var maxHandle = Objects.requireNonNullElse(effectiveStyle.maxHandleBuilder(), DEFAULT_HANDLE_BUILDER).build(!disabled);\n            this.minHandleSize = Objects.requireNonNullElse(effectiveStyle.minHandleSize(), DEFAULT_HANDLE_SIZE);\n            this.maxHandleSize = Objects.requireNonNullElse(effectiveStyle.maxHandleSize(), DEFAULT_HANDLE_SIZE);\n            //noinspection OptionalAssignedToNull\n            var confirmSound = effectiveStyle.confirmSound() != null ? effectiveStyle.confirmSound().orElse(null) : SoundEvents.UI_BUTTON_CLICK.value();\n\n            this.normalizedMin = widget.sliderFunction.normalize(widget.minValue, widget.min, widget.max);\n            this.normalizedMax = widget.sliderFunction.normalize(widget.maxValue, widget.min, widget.max);\n            this.draggingCursorStyle = null;\n\n            return new LayoutBuilder((innerContext, constraints) -> {\n                var combinedHandleSize = this.minHandleSize + this.maxHandleSize;\n                var rangeExtent = Math.ceil((constraints.maxOnAxis(widget.axis) - combinedHandleSize) * (this.normalizedMax - this.normalizedMin));\n\n                var content = new Stack(\n                    widget.axis.choose(Alignment.LEFT, Alignment.TOP),\n                    new Sized(constraints.maxWidth(), constraints.maxHeight(), track),\n                    new Padding(\n                        widget.axis.chooseCompute(\n                            () -> Insets.left(this.minHandleSize + Math.floor((constraints.maxWidth() - this.minHandleSize * 2) * this.normalizedMin) - 1),\n                            () -> Insets.top(this.minHandleSize + Math.floor((constraints.maxHeight() - this.minHandleSize * 2) * this.normalizedMin) - 1)\n                        ),\n                        new Center(\n                            1.0, null,\n                            widget.axis.chooseCompute(\n                                () -> new Sized(rangeExtent + 2, constraints.maxHeight(), rangeIndicator),\n                                () -> new Sized(constraints.maxWidth(), rangeExtent + 2, rangeIndicator)\n                            )\n                        )\n                    ),\n                    new Padding(\n                        widget.axis.chooseCompute(\n                            () -> Insets.left(Math.floor((constraints.maxWidth() - this.minHandleSize * 2) * this.normalizedMin)),\n                            () -> Insets.top(Math.floor((constraints.maxHeight() - this.minHandleSize * 2) * this.normalizedMin))\n                        ),\n                        widget.axis.chooseCompute(\n                            () -> new Sized(this.minHandleSize, constraints.maxHeight(), minHandle),\n                            () -> new Sized(constraints.maxWidth(), this.minHandleSize, minHandle)\n                        )\n                    ),\n                    new Padding(\n                        widget.axis.chooseCompute(\n                            () -> Insets.left(this.maxHandleSize + Math.floor((constraints.maxWidth() - this.maxHandleSize * 2) * this.normalizedMax)),\n                            () -> Insets.top(this.maxHandleSize + Math.floor((constraints.maxHeight() - this.maxHandleSize * 2) * this.normalizedMax))\n                        ),\n                        widget.axis.chooseCompute(\n                            () -> new Sized(this.maxHandleSize, constraints.maxHeight(), maxHandle),\n                            () -> new Sized(constraints.maxWidth(), this.maxHandleSize, maxHandle)\n                        )\n                    )\n                );\n\n                return new Center(\n                    widget.onChanged == null || ControlsOverride.controlsDisabled(context)\n                        ? content\n                        : new Incrementor(\n                            widget.axis,\n                            this::increment,\n                            new MouseArea(\n                                mousearea -> mousearea\n                                    .clickCallback((x, y, button, modifiers) -> {\n                                        if (button != 0) return false;\n\n                                        if (widget.axis == LayoutAxis.VERTICAL) y = constraints.maxFiniteOrMinOnAxis(widget.axis) - y;\n                                        this.grabbedHandle = this.handleAt(constraints, x, y);\n                                        if (this.grabbedHandle == Handle.BOTH && !this.isInRange(constraints, x, y)) {\n                                            this.grabbedHandle = this.nearestHandle(constraints, x, y);\n                                        }\n                                        var initialDragValue = this.grabbedHandle == Handle.MAX ? this.normalizedMax : this.normalizedMin;\n\n                                        if (!this.isInHandle(constraints, x, y) && this.grabbedHandle != Handle.BOTH) {\n                                            initialDragValue = this.setAbsolute(constraints, x, y);\n                                        }\n\n                                        this.dragWidth = this.normalizedMax - this.normalizedMin;\n                                        this.dragValue = initialDragValue;\n                                        this.dragging = true;\n                                        return true;\n                                    })\n                                    .dragCallback((x, y, dx, dy) -> this.move(constraints, dx, widget.axis == LayoutAxis.VERTICAL ? -dy : dy))\n                                    .dragEndCallback(() -> {\n                                        this.dragging = false;\n                                        if (confirmSound != null) {\n                                            UISounds.play(confirmSound);\n                                        }\n                                    })\n                                    .cursorStyleSupplier((x, y) -> {\n                                        if (!isInHandle(constraints, x, constraints.maxHeight() - y) && !isInRange(constraints, x, constraints.maxHeight() - y) && !dragging) return CursorStyle.HAND;\n                                        if (this.isInRange(constraints, x, y)) return CursorStyle.MOVE;\n                                        if (this.draggingCursorStyle == null) this.draggingCursorStyle = CursorStyle.forDraggingAlong(widget.axis, context.instance().computeGlobalTransform());\n                                        return this.draggingCursorStyle;\n                                    }),\n                                content\n                            )\n                        )\n                );\n            });\n        }\n\n        protected Handle handleAt(Constraints constraints, double x, double y) {\n            var widget = this.widget();\n            if (widget.axis == LayoutAxis.VERTICAL) y = constraints.maxFiniteOrMinOnAxis(widget.axis) - y;\n            var coordinate = widget.axis.choose(x, y);\n            var minStart = Math.floor((constraints.maxFiniteOrMinOnAxis(widget.axis) - this.minHandleSize * 2) * this.normalizedMin);\n            var maxStart = this.maxHandleSize + Math.floor((constraints.maxFiniteOrMinOnAxis(widget.axis) - this.maxHandleSize * 2) * this.normalizedMax);\n\n            var inMin = coordinate >= minStart && coordinate <= minStart + this.minHandleSize;\n            var inMax = coordinate >= maxStart && coordinate <= maxStart + this.maxHandleSize;\n\n            if (inMin && inMax) return Handle.BOTH;\n            if (inMin) return Handle.MIN;\n            if (inMax) return Handle.MAX;\n            if (coordinate > minStart + this.minHandleSize && coordinate < maxStart) return Handle.BOTH;\n            var distToMin = Math.abs(coordinate - (minStart + this.minHandleSize / 2));\n            var distToMax = Math.abs(coordinate - (maxStart + this.maxHandleSize / 2));\n            return distToMin <= distToMax ? Handle.MIN : Handle.MAX;\n        }\n\n        protected boolean isInHandle(Constraints constraints, double x, double y) {\n            var widget = this.widget();\n            if (widget.axis == LayoutAxis.VERTICAL) y = constraints.maxFiniteOrMinOnAxis(widget.axis) - y;\n            var coordinate = widget.axis.choose(x, y);\n            var minStart = Math.floor((constraints.maxFiniteOrMinOnAxis(widget.axis) - this.minHandleSize * 2) * this.normalizedMin);\n            var maxStart = this.maxHandleSize + Math.floor((constraints.maxFiniteOrMinOnAxis(widget.axis) - this.maxHandleSize * 2) * this.normalizedMax);\n            return (coordinate >= minStart && coordinate <= minStart + this.minHandleSize) || (coordinate >= maxStart && coordinate <= maxStart + this.maxHandleSize);\n        }\n\n        protected boolean isInRange(Constraints constraints, double x, double y) {\n            var widget = this.widget();\n            if (widget.axis == LayoutAxis.VERTICAL) y = constraints.maxFiniteOrMinOnAxis(widget.axis) - y;\n            var coordinate = widget.axis.choose(x, y);\n            var minEnd = Math.floor((constraints.maxFiniteOrMinOnAxis(widget.axis) - this.minHandleSize * 2) * this.normalizedMin) + this.minHandleSize;\n            var maxStart = this.maxHandleSize + Math.floor((constraints.maxFiniteOrMinOnAxis(widget.axis) - this.maxHandleSize * 2) * this.normalizedMax);\n            return coordinate >= minEnd && coordinate <= maxStart;\n        }\n\n        protected Handle nearestHandle(Constraints constraints, double x, double y) {\n            var widget = this.widget();\n            var trackLength = constraints.maxOnAxis(widget.axis) - (this.minHandleSize + this.maxHandleSize);\n            var minCenter = this.normalizedMin * trackLength + this.minHandleSize / 2;\n            var maxCenter = this.normalizedMax * trackLength + this.maxHandleSize / 2;\n            var coordinate = widget.axis.choose(x, y);\n            return Math.abs(coordinate - minCenter) <= Math.abs(coordinate - maxCenter) ? Handle.MIN : Handle.MAX;\n        }\n\n        protected double normalizedValueAt(Constraints constraints, double x, double y, @Nullable Handle grabbedHandle) {\n            var widget = this.widget();\n            if (widget.axis == LayoutAxis.VERTICAL) y = constraints.maxFiniteOrMinOnAxis(widget.axis) - y;\n            double coordinate = widget.axis.choose(x, y);\n\n            if (grabbedHandle == Handle.MAX) {\n                var denom = Math.max(1, constraints.maxFiniteOrMinOnAxis(widget.axis) - this.maxHandleSize * 2);\n                return Mth.clamp((coordinate - this.maxHandleSize * 1.5) / denom, 0, 1);\n            } else {\n                var denom = Math.max(1, constraints.maxFiniteOrMinOnAxis(widget.axis) - this.minHandleSize * 2);\n                return Mth.clamp((coordinate - this.minHandleSize / 2) / denom, 0, 1);\n            }\n        }\n\n        protected double setAbsolute(Constraints constraints, double x, double y) {\n            if (this.widget().onChanged == null) return this.grabbedHandle == Handle.MAX ? this.normalizedMax : this.normalizedMin;\n            var normalizedValue = this.normalizedValueAt(constraints, x, y, this.grabbedHandle);\n            var newNormalizedMin = this.normalizedMin;\n            var newNormalizedMax = this.normalizedMax;\n            var minRangeNorm = this.minRangeNorm();\n            var maxRangeNorm = this.maxRangeNorm();\n\n            if (this.grabbedHandle == Handle.MIN) {\n                var upper = newNormalizedMax - minRangeNorm;\n                var lower = maxRangeNorm >= 0 ? newNormalizedMax - maxRangeNorm : 0;\n                newNormalizedMin = Mth.clamp(normalizedValue, Math.max(0, lower), Math.max(0, upper));\n            } else if (this.grabbedHandle == Handle.MAX) {\n                var lower = newNormalizedMin + minRangeNorm;\n                var upper = maxRangeNorm >= 0 ? newNormalizedMin + maxRangeNorm : 1;\n                newNormalizedMax = Mth.clamp(normalizedValue, Math.min(1, lower), Math.min(1, upper));\n            }\n\n            this.applyValue(newNormalizedMin, newNormalizedMax);\n\n            return this.grabbedHandle == Handle.MAX ? newNormalizedMax : newNormalizedMin;\n        }\n\n\n        protected void move(Constraints constraints, double dx, double dy) {\n            if (this.widget().onChanged == null || this.grabbedHandle == null) return;\n            var axis = this.widget().axis;\n            var combinedHandleSize = this.minHandleSize + this.maxHandleSize;\n            var track = constraints.maxFiniteOrMinOnAxis(axis) - combinedHandleSize;\n\n            var deltaNorm = (axis.choose(dx, dy)) / track;\n            this.dragValue += deltaNorm;\n            var minRangeNorm = this.minRangeNorm();\n            var maxRangeNorm = this.maxRangeNorm();\n\n            switch (this.grabbedHandle) {\n                case MIN -> {\n                    var maxCap = this.normalizedMax - minRangeNorm;\n                    var minCap = maxRangeNorm >= 0 ? this.normalizedMax - maxRangeNorm : 0;\n                    var newMin = Mth.clamp(this.dragValue, Math.max(0, minCap), Math.max(0, maxCap));\n                    this.applyValue(newMin, this.normalizedMax);\n                }\n                case MAX -> {\n                    var minCap = this.normalizedMin + minRangeNorm;\n                    var maxCap = maxRangeNorm >= 0 ? this.normalizedMin + maxRangeNorm : 1;\n                    var newMax = Mth.clamp(this.dragValue, Math.min(1, minCap), Math.min(1, maxCap));\n                    this.applyValue(this.normalizedMin, newMax);\n                }\n                case BOTH -> {\n                    var width = this.dragWidth;\n                    this.dragValue = Mth.clamp(this.dragValue, 0, 1 - width);\n                    var newMin = this.dragValue;\n                    var newMax = newMin + width;\n                    this.applyValue(newMin, newMax);\n                }\n            }\n        }\n\n        protected void increment(double increment) {\n            if (this.widget().onChanged == null) return;\n            var target = this.grabbedHandle != null ? this.grabbedHandle : Handle.BOTH;\n            var delta = this.incrementStep * increment;\n            var newMin = this.normalizedMin;\n            var newMax = this.normalizedMax;\n            var minRangeNorm = this.minRangeNorm();\n            var maxRangeNorm = this.maxRangeNorm();\n            if (target == Handle.MIN) {\n                var upper = newMax - minRangeNorm;\n                var lower = maxRangeNorm >= 0 ? newMax - maxRangeNorm : 0;\n                newMin = Mth.clamp(newMin + delta, Math.max(0, lower), Math.max(0, upper));\n            } else if (target == Handle.MAX) {\n                var lower = newMin + minRangeNorm;\n                var upper = maxRangeNorm >= 0 ? newMin + maxRangeNorm : 1;\n                newMax = Mth.clamp(newMax + delta, Math.min(1, lower), Math.min(1, upper));\n            } else {\n                newMin = Mth.clamp(newMin + delta, 0, 1);\n                newMax = Mth.clamp(newMax + delta, 0, 1);\n            }\n            this.applyValue(newMin, newMax);\n        }\n\n        protected void applyValue(double newNormalizedMin, double newNormalizedMax) {\n            var widget = this.widget();\n\n            var newMinValue = widget.sliderFunction.deNormalize(newNormalizedMin, widget.min, widget.max);\n            var newMaxValue = widget.sliderFunction.deNormalize(newNormalizedMax, widget.min, widget.max);\n\n            if (widget.step != null) {\n                var step = widget.step;\n                newMinValue = Math.round(newMinValue / step) * step;\n                newMaxValue = Math.round(newMaxValue / step) * step;\n            }\n\n            newMinValue = Mth.clamp(newMinValue, widget.min, widget.max);\n            newMaxValue = Mth.clamp(newMaxValue, widget.min, widget.max);\n\n            widget.onChanged.accept(newMinValue, newMaxValue);\n        }\n\n        protected double minRangeNorm() {\n            var widget = this.widget();\n            if (widget.minRange <= 0) return 0;\n            var a = widget.sliderFunction.normalize(widget.min, widget.min, widget.max);\n            var b = widget.sliderFunction.normalize(widget.min + widget.minRange, widget.min, widget.max);\n            return Math.max(0, b - a);\n        }\n\n        protected double maxRangeNorm() {\n            var widget = this.widget();\n            if (widget.maxRange < 0) return -1;\n            var a = widget.sliderFunction.normalize(widget.min, widget.min, widget.max);\n            var b = widget.sliderFunction.normalize(widget.min + widget.maxRange, widget.min, widget.max);\n            return Math.max(0, b - a);\n        }\n\n        protected enum Handle {\n            MIN, MAX, BOTH\n        }\n    }\n\n    // ---\n\n    private static final Widget DEFAULT_TRACK = new Panel(ButtonComponent.DISABLED_TEXTURE);\n    private static final Widget DEFAULT_RANGE_INDICATOR = new Box(new Color(0x7f000000));\n    private static final RangeSliderStyle.HandleBuilder DEFAULT_HANDLE_BUILDER = DefaultSliderHandle::new;\n    private static final double DEFAULT_HANDLE_SIZE = 8.0;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/range/RangeSliderCallback.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.range;\n\n@FunctionalInterface\npublic interface RangeSliderCallback {\n    void accept(double newMin, double newMax);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/range/RangeSliderStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.range;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport net.minecraft.sounds.SoundEvent;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Optional;\n\npublic record RangeSliderStyle(\n    @Nullable Widget track,\n    @Nullable Widget rangeIndicator,\n    @Nullable HandleBuilder minHandleBuilder,\n    @Nullable Double minHandleSize,\n    @Nullable HandleBuilder maxHandleBuilder,\n    @Nullable Double maxHandleSize,\n    @Nullable Optional<SoundEvent> confirmSound\n) {\n    public RangeSliderStyle overriding(RangeSliderStyle other) {\n        //noinspection OptionalAssignedToNull\n        return new RangeSliderStyle(\n            this.track != null ? this.track : other.track,\n            this.rangeIndicator != null ? this.rangeIndicator : other.rangeIndicator,\n            this.minHandleBuilder != null ? this.minHandleBuilder : other.minHandleBuilder,\n            this.minHandleSize != null ? this.minHandleSize : other.minHandleSize,\n            this.maxHandleBuilder != null ? this.maxHandleBuilder : other.maxHandleBuilder,\n            this.maxHandleSize != null ? this.maxHandleSize : other.maxHandleSize,\n            this.confirmSound != null ? this.confirmSound : other.confirmSound\n        );\n    }\n\n    public static final RangeSliderStyle DEFAULT = new RangeSliderStyle(null, null, null, null, null, null, null);\n\n    @FunctionalInterface\n    public interface HandleBuilder {\n        Widget build(boolean active);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/slider/DefaultSliderStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.slider;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.slider.SliderStyle;\nimport org.jetbrains.annotations.Nullable;\n\npublic class DefaultSliderStyle extends InheritedWidget {\n\n    public final SliderStyle<Double> style;\n\n    public DefaultSliderStyle(SliderStyle<Double> style, Widget child) {\n        super(child);\n        this.style = style;\n    }\n\n    public static Widget merge(SliderStyle<Double> style, Widget child) {\n        return new Builder(context -> {\n            var contextStyle = DefaultSliderStyle.maybeOf(context);\n            return new DefaultSliderStyle(contextStyle != null ? style.overriding(contextStyle) : style, child);\n        });\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return !this.style.equals(((DefaultSliderStyle) newWidget).style);\n    }\n\n    public static @Nullable SliderStyle<Double> maybeOf(BuildContext context) {\n        var widget = context.dependOnAncestor(DefaultSliderStyle.class);\n        if (widget != null) {\n            return widget.style;\n        } else {\n            return null;\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/slider/MessageSlider.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.slider;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\npublic class MessageSlider extends StatelessWidget {\n\n    public final double value;\n    public final @Nullable WidgetSetupCallback<Slider> setupCallback;\n    public final @Nullable SliderCallback onChanged;\n\n    public final Component message;\n\n    public MessageSlider(\n        double value,\n        Component message,\n        @Nullable WidgetSetupCallback<Slider> setupCallback,\n        @Nullable SliderCallback onChanged\n    ) {\n        this.value = value;\n        this.setupCallback = setupCallback;\n        this.onChanged = onChanged;\n        this.message = message;\n    }\n\n    public MessageSlider(\n        double value,\n        Component message,\n        @Nullable WidgetSetupCallback<Slider> setupCallback,\n        boolean active,\n        SliderCallback onChanged\n    ) {\n        this(value, message, setupCallback, active ? onChanged : null);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Stack(\n            new Slider(\n                this.value,\n                this.setupCallback, this.onChanged\n            ),\n            //TODO: abstract this styling?\n            new Label(\n                LabelStyle.SHADOW,\n                false,\n                this.message\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/slider/Slider.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.slider;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.slider.DefaultSliderHandle;\nimport io.wispforest.owo.braid.widgets.slider.Incrementor;\nimport io.wispforest.owo.braid.widgets.slider.SliderStyle;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.util.UISounds;\nimport net.minecraft.sounds.SoundEvents;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\n\npublic class Slider extends StatefulWidget {\n\n    public final double value;\n    public final @Nullable SliderCallback onChanged;\n\n    protected double min = 0;\n    protected double max = 1;\n    protected @Nullable Double step;\n    protected SliderFunction function = SliderFunction.LINEAR;\n    protected LayoutAxis axis = LayoutAxis.HORIZONTAL;\n    protected @Nullable Double incrementStep = null;\n    protected @Nullable SliderStyle<Double> style;\n\n    public Slider(\n        double value,\n        @Nullable WidgetSetupCallback<Slider> setupCallback,\n        @Nullable SliderCallback onChanged\n    ) {\n        this.value = value;\n        this.onChanged = onChanged;\n        if (setupCallback != null) setupCallback.setup(this);\n    }\n\n    public Slider(\n        double value,\n        @Nullable WidgetSetupCallback<Slider> setupCallback,\n        boolean active,\n        SliderCallback onChanged\n    ) {\n        this(value, setupCallback, active ? onChanged : null);\n    }\n\n    public Slider min(double min) {\n        this.assertMutable();\n        this.min = min;\n        return this;\n    }\n\n    public double min() {\n        return this.min;\n    }\n\n    public Slider max(double max) {\n        this.assertMutable();\n        this.max = max;\n        return this;\n    }\n\n    public double max() {\n        return this.max;\n    }\n\n    public Slider range(double min, double max) {\n        this.assertMutable();\n        this.min = min;\n        this.max = max;\n        return this;\n    }\n\n    public Slider step(@Nullable Double step) {\n        this.assertMutable();\n        this.step = step;\n        return this;\n    }\n\n    public Slider step(double step) {\n        this.assertMutable();\n        this.step = step;\n        return this;\n    }\n\n    public @Nullable Double step() {\n        return this.step;\n    }\n\n    public Slider function(SliderFunction sliderFunction) {\n        this.assertMutable();\n        this.function = sliderFunction;\n        return this;\n    }\n\n    public SliderFunction function() {\n        return this.function;\n    }\n\n    public Slider axis(LayoutAxis axis) {\n        this.assertMutable();\n        this.axis = axis;\n        return this;\n    }\n\n    public Slider vertical() {\n        return this.axis(LayoutAxis.VERTICAL);\n    }\n\n    public LayoutAxis axis() {\n        return this.axis;\n    }\n\n    public Slider incrementStep(double incrementStep) {\n        this.assertMutable();\n        this.incrementStep = incrementStep;\n        return this;\n    }\n\n    public @Nullable Double incrementStep() {\n        return this.incrementStep;\n    }\n\n    public Slider style(SliderStyle<Double> style) {\n        this.assertMutable();\n        this.style = style;\n        return this;\n    }\n\n    public @Nullable SliderStyle<Double> style() {\n        return this.style;\n    }\n\n    @Override\n    public WidgetState<?> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<Slider> {\n\n        protected double dragValue = 0;\n        protected boolean dragging = false;\n\n        protected double normalizedValue;\n        protected double incrementStep;\n        protected CursorStyle draggingCursorStyle = null;\n\n        protected double handleSize;\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            var effectiveStyle = widget.style != null ? widget.style : SliderStyle.<Double>getDefault();\n            if (DefaultSliderStyle.maybeOf(context) instanceof SliderStyle<Double> contextStyle) {\n                effectiveStyle = effectiveStyle.overriding(contextStyle);\n            }\n\n            var disabled = widget.onChanged == null || ControlsOverride.controlsDisabled(context);\n\n            var track = Objects.requireNonNullElse(effectiveStyle.track(), DEFAULT_TRACK);\n            var handle = Objects.requireNonNullElse(effectiveStyle.handleBuilder(), DEFAULT_HANDLE_BUILDER).build(!disabled);\n            this.handleSize = Objects.requireNonNullElse(effectiveStyle.handleSize(), DEFAULT_HANDLE_SIZE);\n            //noinspection OptionalAssignedToNull\n            var confirmSound = effectiveStyle.confirmSound() != null ? effectiveStyle.confirmSound().orElse(null) : SoundEvents.UI_BUTTON_CLICK.value();\n\n            this.normalizedValue = widget.function.normalize(widget.value, widget.min, widget.max);\n            var trueMin = Math.min(widget.max, widget.min);\n            var trueMax = Math.max(widget.max, widget.min);\n            this.incrementStep = widget.incrementStep != null\n                ? widget.function.normalize(widget.incrementStep, trueMin, trueMax)\n                : widget.step != null\n                    ? widget.function.normalize(widget.step, trueMin, trueMax)\n                    : 0.01;\n            this.draggingCursorStyle = null;\n\n            return new LayoutBuilder((innerContext, constraints) -> {\n                var size = constraints.maxFiniteOrMinSize();\n                var content = new Stack(\n                    widget.axis.choose(Alignment.LEFT, Alignment.TOP),\n                    new Sized(size, track),\n                    new Padding(\n                        widget.axis.chooseCompute(\n                            () -> Insets.left(Math.floor((size.width() - this.handleSize) * this.normalizedValue)),\n                            () -> Insets.top(Math.floor((size.height() - this.handleSize) * (1 - this.normalizedValue)))\n                        ),\n                        widget.axis.chooseCompute(\n                            () -> new Sized(this.handleSize, size.height(), handle),\n                            () -> new Sized(size.width(), this.handleSize, handle)\n                        )\n                    )\n                );\n                return new Center(\n                    widget.onChanged == null || ControlsOverride.controlsDisabled(context)\n                        ? content\n                        : new Incrementor(\n                            widget.axis,\n                            increment -> this.applyValue(Mth.clamp(this.normalizedValue + this.incrementStep * increment, 0, 1)),\n                            new MouseArea(\n                                mouseArea -> mouseArea\n                                    //TODO: decide what to do with buttons here\n                                    .clickCallback((x, y, button, modifiers) -> {\n                                        if (button != 0) return false;\n\n                                        if (widget.axis == LayoutAxis.VERTICAL) y = constraints.maxFiniteOrMinOnAxis(widget.axis) - y;\n                                        var initialDragValue = this.normalizedValue;\n                                        if (!this.isInHandle(constraints, x, y)) initialDragValue = this.setAbsolute(constraints, x, y);\n\n                                        this.dragValue = initialDragValue;\n                                        this.dragging = true;\n                                        return true;\n                                    })\n                                    .dragCallback((x, y, dx, dy) -> this.move(constraints, dx, widget.axis == LayoutAxis.VERTICAL ? -dy : dy))\n                                    .dragEndCallback(() -> {\n                                        this.dragging = false;\n                                        if (confirmSound != null) {\n                                            UISounds.play(confirmSound);\n                                        }\n                                    })\n                                    .cursorStyleSupplier((x, y) -> {\n                                        //TODO: invert the y passed in here cuz its cringe atm\n                                        if (!this.isInHandle(constraints, x, constraints.maxHeight() - y) && !this.dragging) return CursorStyle.HAND;\n                                        if (this.draggingCursorStyle == null) this.draggingCursorStyle = CursorStyle.forDraggingAlong(widget.axis, context.instance().computeGlobalTransform());\n                                        return this.draggingCursorStyle;\n                                    }),\n                                content\n                            )\n                        )\n                );\n            });\n        }\n\n        protected boolean isInHandle(Constraints constraints, double x, double y) {\n            var axis = this.widget().axis;\n\n            var trackLength = constraints.maxFiniteOrMinOnAxis(axis) - this.handleSize;\n            var handleMin = this.normalizedValue * trackLength;\n            var handleMax = handleMin + this.handleSize;\n\n            var coordinate = axis.choose(x, y);\n            return coordinate >= handleMin && coordinate <= handleMax;\n        }\n\n        protected void move(Constraints constraints, double dx, double dy) {\n            this.dragValue += this.widget().axis.choose(dx, dy) / (constraints.maxFiniteOrMinOnAxis(this.widget().axis) - this.handleSize);\n\n            this.applyValue(Mth.clamp(this.dragValue, 0, 1));\n        }\n\n        protected double setAbsolute(Constraints constraints, double x, double y) {\n            if (this.widget().onChanged == null) return this.normalizedValue;\n\n            var axis = this.widget().axis;\n            var newNormalizedValue = Mth.clamp((axis.choose(x, y) - this.handleSize / 2) / (constraints.maxFiniteOrMinOnAxis(axis) - this.handleSize), 0, 1);\n\n            this.applyValue(newNormalizedValue);\n            return newNormalizedValue;\n        }\n\n        protected void applyValue(double newNormalizedValue) {\n            var widget = this.widget();\n            var step = widget.step;\n            var newValue = widget.function.deNormalize(newNormalizedValue, widget.min, widget.max);\n            this.widget().onChanged.accept(step != null ? Math.round(newValue / step) * step : newValue);\n        }\n    }\n\n    // ---\n\n    private static final Widget DEFAULT_TRACK = new Panel(ButtonComponent.DISABLED_TEXTURE);\n    private static final SliderStyle.HandleBuilder DEFAULT_HANDLE_BUILDER = DefaultSliderHandle::new;\n    private static final double DEFAULT_HANDLE_SIZE = 8.0;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/slider/SliderCallback.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.slider;\n\n@FunctionalInterface\npublic interface SliderCallback {\n    void accept(double newValue);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/slider/SliderFunction.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.slider;\n\nimport net.minecraft.util.Mth;\n\nimport static net.minecraft.util.Mth.EPSILON;\n\npublic interface SliderFunction {\n    double normalize(double value, double min, double max);\n\n    double deNormalize(double normalizedValue, double min, double max);\n\n    SliderFunction LINEAR = new SliderFunction() {\n        @Override\n        public double normalize(double value, double min, double max) {\n            return (value - min) / (max - min);\n        }\n\n        @Override\n        public double deNormalize(double normalizedValue, double min, double max) {\n            return min + normalizedValue * (max - min);\n        }\n    };\n\n    SliderFunction LOGARITHMIC = new SliderFunction() {\n\n        @Override\n        public double normalize(double value, double min, double max) {\n            if (min <= 0) {\n                var offset = EPSILON - min;\n                min += offset;\n                max += offset;\n                value += offset;\n            }\n\n            value = Mth.clamp(value, min, max);\n\n            var logMin = Math.log(min);\n            var logMax = Math.log(max);\n\n            if (logMin >= logMax) return (value - min) / (max - min);\n\n            return (Math.log(value) - logMin) / (logMax - logMin);\n        }\n\n        @Override\n        public double deNormalize(double normalizedValue, double min, double max) {\n            if (min <= 0) {\n                var offset = EPSILON - min;\n                min += offset;\n                max += offset;\n            }\n\n            var logMin = Math.log(min);\n            var logMax = Math.log(max);\n\n            var expValue = Math.exp(logMin + normalizedValue * (logMax - logMin));\n\n            if (min <= 0 && max > min) expValue -= (EPSILON - min);\n\n            return expValue;\n        }\n    };\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/xlyder/DefaultXlyderStyle.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.xlyder;\n\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.slider.SliderStyle;\nimport org.jetbrains.annotations.Nullable;\n\npublic class DefaultXlyderStyle extends InheritedWidget {\n\n    public final SliderStyle<Size> style;\n\n    public DefaultXlyderStyle(SliderStyle<Size> style, Widget child) {\n        super(child);\n        this.style = style;\n    }\n\n    public static Widget merge(SliderStyle<Size> style, Widget child) {\n        return new Builder(context -> {\n            var contextStyle = DefaultXlyderStyle.maybeOf(context);\n            return new DefaultXlyderStyle(contextStyle != null ? style.overriding(contextStyle) : style, child);\n        });\n    }\n\n    @Override\n    public boolean mustRebuildDependents(InheritedWidget newWidget) {\n        return !this.style.equals(((DefaultXlyderStyle) newWidget).style);\n    }\n\n    public static @Nullable SliderStyle<Size> maybeOf(BuildContext context) {\n        var widget = context.dependOnAncestor(DefaultXlyderStyle.class);\n        if (widget != null) {\n            return widget.style;\n        } else {\n            return null;\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/xlyder/MessageXlyder.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.xlyder;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\nimport org.joml.Vector2dc;\n\npublic class MessageXlyder extends StatelessWidget {\n\n    public final Vector2dc value;\n    public final @Nullable WidgetSetupCallback<Xlyder> setupCallback;\n    public final @Nullable XlyderCallback onChanged;\n\n    public final Component message;\n\n    public MessageXlyder(\n        Vector2dc value,\n        Component message,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        @Nullable XlyderCallback onChanged\n    ) {\n        this.value = value;\n        this.setupCallback = setupCallback;\n        this.onChanged = onChanged;\n        this.message = message;\n    }\n\n    public MessageXlyder(\n        Vector2dc value,\n        Component message,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        boolean active,\n        XlyderCallback onChanged\n    ) {\n        this(value, message, setupCallback, active ? onChanged : null);\n    }\n\n    public MessageXlyder(\n        double x, double y,\n        Component message,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        @Nullable XlyderCallback onChanged\n    ) {\n        this(new Vector2d(x, y), message, setupCallback, onChanged);\n    }\n\n    public MessageXlyder(\n        double x, double y,\n        Component message,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        boolean active,\n        XlyderCallback onChanged\n    ) {\n        this(new Vector2d(x, y), message, setupCallback, active ? onChanged : null);\n    }\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new Stack(\n            new Xlyder(\n                this.value,\n                this.setupCallback, this.onChanged\n            ),\n            //TODO: abstract this styling?\n            new Label(\n                LabelStyle.SHADOW,\n                false,\n                this.message\n            )\n        );\n    }\n\n    @FunctionalInterface\n    public interface XlyderMessageProvider {\n        Component getMessage(double x, double y);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/xlyder/Xlyder.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.xlyder;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.slider.DefaultSliderHandle;\nimport io.wispforest.owo.braid.widgets.slider.Incrementor;\nimport io.wispforest.owo.braid.widgets.slider.SliderStyle;\nimport io.wispforest.owo.braid.widgets.slider.slider.SliderFunction;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.util.UISounds;\nimport net.minecraft.sounds.SoundEvents;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\nimport org.joml.Vector2dc;\n\nimport java.util.Objects;\n\npublic class Xlyder extends StatefulWidget {\n\n    protected final Vector2d min = new Vector2d();\n    protected final Vector2d max = new Vector2d(1);\n    protected @Nullable Double xStep;\n    protected @Nullable Double yStep;\n    protected SliderFunction xSliderFunction = SliderFunction.LINEAR;\n    protected SliderFunction ySliderFunction = SliderFunction.LINEAR;\n    protected @Nullable Double xIncrementStep = null;\n    protected @Nullable Double yIncrementStep = null;\n    protected SliderStyle<Size> style;\n\n    public final Vector2dc value;\n    public final @Nullable XlyderCallback onChanged;\n\n    public Xlyder(\n        Vector2dc value,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        @Nullable XlyderCallback onChanged\n    ) {\n        this.value = value;\n        this.onChanged = onChanged;\n        if (setupCallback != null) setupCallback.setup(this);\n    }\n\n    public Xlyder(\n        Vector2dc value,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        boolean active,\n        XlyderCallback onChanged\n    ) {\n        this(value, setupCallback, active ? onChanged : null);\n    }\n\n    public Xlyder(\n        double x, double y,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        @Nullable XlyderCallback onChanged\n    ) {\n        this(new Vector2d(x, y), setupCallback, onChanged);\n    }\n\n    public Xlyder(\n        double x, double y,\n        @Nullable WidgetSetupCallback<Xlyder> setupCallback,\n        boolean active,\n        XlyderCallback onChanged\n    ) {\n        this(new Vector2d(x, y), setupCallback, active ? onChanged : null);\n    }\n\n    public Xlyder min(Vector2d min) {\n        this.assertMutable();\n        this.min.set(min);\n        return this;\n    }\n\n    public Xlyder min(double minX, double minY) {\n        this.assertMutable();\n        this.min.set(minX, minY);\n        return this;\n    }\n\n    public Xlyder min(double min) {\n        this.assertMutable();\n        this.min.set(min, min);\n        return this;\n    }\n\n    public Vector2dc min() {\n        return this.min;\n    }\n\n    public Xlyder minX(double minX) {\n        this.assertMutable();\n        this.min.x = minX;\n        return this;\n    }\n\n    public double minX() {\n        return this.min.x;\n    }\n\n    public Xlyder minY(double minY) {\n        this.assertMutable();\n        this.min.y = minY;\n        return this;\n    }\n\n    public double minY() {\n        return this.min.y;\n    }\n\n    public Xlyder max(Vector2d max) {\n        this.assertMutable();\n        this.max.set(max);\n        return this;\n    }\n\n    public Xlyder max(double maxX, double maxY) {\n        this.assertMutable();\n        this.max.set(maxX, maxY);\n        return this;\n    }\n\n    public Xlyder max(double max) {\n        this.assertMutable();\n        this.max.set(max, max);\n        return this;\n    }\n\n    public Vector2dc max() {\n        return this.max;\n    }\n\n    public Xlyder maxX(double maxX) {\n        this.assertMutable();\n        this.max.x = maxX;\n        return this;\n    }\n\n    public double maxX() {\n        return this.max.x;\n    }\n\n    public Xlyder maxY(double maxY) {\n        this.assertMutable();\n        this.max.y = maxY;\n        return this;\n    }\n\n    public double maxY() {\n        return this.max.y;\n    }\n\n    public Xlyder range(Vector2d min, Vector2d max) {\n        this.assertMutable();\n        this.min.set(min);\n        this.max.set(max);\n        return this;\n    }\n\n    public Xlyder range(double minX, double minY, double maxX, double maxY) {\n        this.assertMutable();\n        this.min.set(minX, minY);\n        this.max.set(maxX, maxY);\n        return this;\n    }\n\n    public Xlyder range(double min, double max) {\n        this.assertMutable();\n        this.min.set(min, min);\n        this.max.set(max, max);\n        return this;\n    }\n\n    public Xlyder rangeX(double minX, double maxX) {\n        this.assertMutable();\n        this.min.x = minX;\n        this.max.x = maxX;\n        return this;\n    }\n\n    public Xlyder rangeY(double minY, double maxY) {\n        this.assertMutable();\n        this.min.y = minY;\n        this.max.y = maxY;\n        return this;\n    }\n\n    public Xlyder step(@Nullable Double step) {\n        this.assertMutable();\n        this.xStep = step;\n        this.yStep = step;\n        return this;\n    }\n\n    public Xlyder step(double step) {\n        this.assertMutable();\n        this.xStep = step;\n        this.yStep = step;\n        return this;\n    }\n\n    public Xlyder stepX(@Nullable Double xStep) {\n        this.assertMutable();\n        this.xStep = xStep;\n        return this;\n    }\n\n    public Xlyder stepX(double xStep) {\n        this.assertMutable();\n        this.xStep = xStep;\n        return this;\n    }\n\n    public @Nullable Double stepX() {\n        return this.xStep;\n    }\n\n    public Xlyder stepY(@Nullable Double yStep) {\n        this.assertMutable();\n        this.yStep = yStep;\n        return this;\n    }\n\n    public Xlyder stepY(double yStep) {\n        this.assertMutable();\n        this.yStep = yStep;\n        return this;\n    }\n\n    public @Nullable Double stepY() {\n        return this.yStep;\n    }\n\n    public Xlyder sliderFunction(SliderFunction sliderFunction) {\n        this.assertMutable();\n        this.xSliderFunction = sliderFunction;\n        this.ySliderFunction = sliderFunction;\n        return this;\n    }\n\n    public Xlyder sliderFunctionX(SliderFunction xSliderFunction) {\n        this.assertMutable();\n        this.xSliderFunction = xSliderFunction;\n        return this;\n    }\n\n    public SliderFunction sliderFunctionX() {\n        return this.xSliderFunction;\n    }\n\n    public Xlyder sliderFunctionY(SliderFunction ySliderFunction) {\n        this.assertMutable();\n        this.ySliderFunction = ySliderFunction;\n        return this;\n    }\n\n    public SliderFunction sliderFunctionY() {\n        return this.ySliderFunction;\n    }\n\n    public Xlyder incrementStep(@Nullable Double incrementStep) {\n        this.assertMutable();\n        this.xIncrementStep = incrementStep;\n        this.yIncrementStep = incrementStep;\n        return this;\n    }\n\n    public Xlyder incrementStep(double incrementStep) {\n        this.assertMutable();\n        this.xIncrementStep = incrementStep;\n        this.yIncrementStep = incrementStep;\n        return this;\n    }\n\n    public Xlyder incrementStepX(@Nullable Double xIncrementStep) {\n        this.assertMutable();\n        this.xIncrementStep = xIncrementStep;\n        return this;\n    }\n\n    public Xlyder incrementStepX(double xIncrementStep) {\n        this.assertMutable();\n        this.xIncrementStep = xIncrementStep;\n        return this;\n    }\n\n    public @Nullable Double incrementStepX() {\n        return this.xIncrementStep;\n    }\n\n    public Xlyder incrementStepY(@Nullable Double yIncrementStep) {\n        this.assertMutable();\n        this.yIncrementStep = yIncrementStep;\n        return this;\n    }\n\n    public Xlyder incrementStepY(double yIncrementStep) {\n        this.assertMutable();\n        this.yIncrementStep = yIncrementStep;\n        return this;\n    }\n\n    public @Nullable Double incrementStepY() {\n        return this.yIncrementStep;\n    }\n\n    public Xlyder style(SliderStyle<Size> style) {\n        this.assertMutable();\n        this.style = style;\n        return this;\n    }\n\n    public @Nullable SliderStyle<Size> style() {\n        return this.style;\n    }\n\n    @Override\n    public WidgetState<?> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<Xlyder> {\n\n        protected final Vector2d dragValue = new Vector2d();\n        protected boolean dragging = false;\n\n        protected Vector2dc normalizedValue;\n        protected Vector2dc incrementStep;\n\n        protected Size handleSize;\n\n        @Override\n        public void init() {\n            var widget = this.widget();\n            var trueMinX = Math.min(widget.min.x, widget.max.x);\n            var trueMaxX = Math.max(widget.min.x, widget.max.x);\n            var trueMinY = Math.min(widget.min.y, widget.max.y);\n            var trueMaxY = Math.max(widget.min.y, widget.max.y);\n            this.incrementStep = new Vector2d(\n                widget.xIncrementStep != null ? widget.xSliderFunction.normalize(widget.xIncrementStep, trueMinX, trueMaxX) : widget.xStep != null ? widget.xSliderFunction.normalize(widget.xStep, trueMinX, trueMaxX) : 0.01,\n                widget.yIncrementStep != null ? widget.ySliderFunction.normalize(widget.yIncrementStep, trueMinY, trueMaxY) : widget.yStep != null ? widget.ySliderFunction.normalize(widget.yStep, trueMinY, trueMaxY) : 0.01\n            );\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            var effectiveStyle = widget.style != null ? widget.style : SliderStyle.<Size>getDefault();\n            if (DefaultXlyderStyle.maybeOf(context) instanceof SliderStyle<Size> contextStyle) {\n                effectiveStyle = effectiveStyle.overriding(contextStyle);\n            }\n\n            var disabled = widget.onChanged == null || ControlsOverride.controlsDisabled(context);\n\n            var track = Objects.requireNonNullElse(effectiveStyle.track(), DEFAULT_TRACK);\n            var handle = Objects.requireNonNullElse(effectiveStyle.handleBuilder(), DEFAULT_HANDLE_BUILDER).build(!disabled);\n            this.handleSize = Objects.requireNonNullElse(effectiveStyle.handleSize(), DEFAULT_HANDLE_SIZE);\n            //noinspection OptionalAssignedToNull\n            var confirmSound = effectiveStyle.confirmSound() != null ? effectiveStyle.confirmSound().orElse(null) : SoundEvents.UI_BUTTON_CLICK.value();\n\n            this.normalizedValue = new Vector2d(\n                widget.xSliderFunction.normalize(widget.value.x(), widget.min.x, widget.max.x),\n                widget.ySliderFunction.normalize(widget.value.y(), widget.min.y, widget.max.y)\n            );\n            return new LayoutBuilder((innerContext, constraints) -> {\n                var content = new Stack(\n                    Alignment.TOP_LEFT,\n                    new Sized(constraints.maxWidth(), constraints.maxHeight(), track),\n                    new Padding(\n                        Insets.left(Math.floor((constraints.maxWidth() - this.handleSize.width()) * this.normalizedValue.x()))\n                            .withTop(Math.floor((constraints.maxHeight() - this.handleSize.height()) * (1 - this.normalizedValue.y()))),\n                        new Sized(this.handleSize, handle)\n                    )\n                );\n                return new Center(\n                    widget.onChanged == null || ControlsOverride.controlsDisabled(context)\n                        ? content\n                        : new Incrementor(\n                            xIncrement -> this.applyValue(Mth.clamp(this.normalizedValue.x() + this.incrementStep.x() * xIncrement, 0, 1), null),\n                            yIncrement -> this.applyValue(null, Mth.clamp(this.normalizedValue.y() + this.incrementStep.y() * yIncrement, 0, 1)),\n                            new MouseArea(\n                                mouseArea -> mouseArea\n                                    //TODO: decide what to do with buttons here\n                                    .clickCallback((x, y, button, modifiers) -> {\n                                        if (button != 0) return false;\n\n                                        y = constraints.maxHeight() - y;\n                                        Vector2dc initialDragValue = new Vector2d(this.normalizedValue);\n                                        if (!this.isInHandle(constraints, x, y)) initialDragValue = this.setAbsolute(constraints, x, y);\n\n                                        this.dragValue.set(initialDragValue);\n                                        this.dragging = true;\n                                        return true;\n                                    })\n                                    .dragCallback((x, y, dx, dy) -> this.move(constraints, dx, -dy))\n                                    .dragEndCallback(() -> {\n                                        this.dragging = false;\n                                        if (confirmSound != null) {\n                                            UISounds.play(confirmSound);\n                                        }\n                                    })\n                                    //TODO: invert the y passed here cuz it cringe atm\n                                    .cursorStyleSupplier((x, y) -> (!this.isInHandle(constraints, x, constraints.maxHeight() - y) && !this.dragging) ? CursorStyle.HAND : CursorStyle.MOVE),\n                                content\n                            )\n                        )\n                );\n            });\n        }\n\n        protected boolean isInHandle(Constraints constraints, double x, double y) {\n            var trackWidth = constraints.maxWidth() - this.handleSize.width();\n            var trackHeight = constraints.maxHeight() - this.handleSize.height();\n\n            var handleMinX = this.normalizedValue.x() * trackWidth;\n            var handleMinY = this.normalizedValue.y() * trackHeight;\n            var handleMaxX = handleMinX + this.handleSize.width();\n            var handleMaxY = handleMinY + this.handleSize.height();\n\n            return x >= handleMinX && x <= handleMaxX && y >= handleMinY && y <= handleMaxY;\n        }\n\n        protected void move(Constraints constraints, double dx, double dy) {\n            this.dragValue.add(\n                dx / (constraints.maxWidth() - this.handleSize.width()),\n                dy / (constraints.maxHeight() - this.handleSize.height())\n            );\n\n            this.applyValue(\n                Mth.clamp(this.dragValue.x, 0, 1),\n                Mth.clamp(this.dragValue.y, 0, 1)\n            );\n        }\n\n        protected Vector2dc setAbsolute(Constraints constraints, double x, double y) {\n            if (this.widget().onChanged == null) return this.normalizedValue;\n\n            var handleSize = this.handleSize;\n\n            var newNormalizedX = Mth.clamp((x - (handleSize.width() / 2)) / (constraints.maxWidth() - handleSize.width()), 0, 1);\n            var newNormalizedY = Mth.clamp((y - (handleSize.height() / 2)) / (constraints.maxHeight() - handleSize.height()), 0, 1);\n\n            this.applyValue(newNormalizedX, newNormalizedY);\n            return new Vector2d(newNormalizedX, newNormalizedY);\n        }\n\n        protected void applyValue(@Nullable Double newNormalizedX, @Nullable Double newNormalizedY) {\n            if (newNormalizedX == null && newNormalizedY == null) return;\n            var widget = this.widget();\n            double newX = widget.value.x();\n            double newY = widget.value.y();\n            if (newNormalizedX != null) newX = widget.xSliderFunction.deNormalize(newNormalizedX, widget.min.x, widget.max.x);\n            if (newNormalizedY != null) newY = widget.ySliderFunction.deNormalize(newNormalizedY, widget.min.y, widget.max.y);\n            widget.onChanged.accept(\n                widget.xStep != null ? Math.round(newX / widget.xStep) * widget.xStep : newX,\n                widget.yStep != null ? Math.round(newY / widget.yStep) * widget.yStep : newY\n            );\n        }\n    }\n\n    // ---\n\n    private static final Widget DEFAULT_TRACK = new Panel(ButtonComponent.DISABLED_TEXTURE);\n    private static final SliderStyle.HandleBuilder DEFAULT_HANDLE_BUILDER = DefaultSliderHandle::new;\n    private static final Size DEFAULT_HANDLE_SIZE = Size.square(8.0);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/slider/xlyder/XlyderCallback.java",
    "content": "package io.wispforest.owo.braid.widgets.slider.xlyder;\n\n@FunctionalInterface\npublic interface XlyderCallback {\n    void accept(double newX, double newY);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/splitpane/MultiSplitPane.java",
    "content": "package io.wispforest.owo.braid.widgets.splitpane;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.Key;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Constrain;\nimport io.wispforest.owo.braid.widgets.basic.LayoutBuilder;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Flex;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport net.minecraft.util.Mth;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n\npublic class MultiSplitPane extends StatefulWidget {\n\n    public final LayoutAxis mainAxis;\n    public final MainAxisAlignment mainAxisAlignment;\n    public final CrossAxisAlignment crossAxisAlignment;\n    public final List<? extends Widget> children;\n\n    public MultiSplitPane(\n        LayoutAxis mainAxis,\n        MainAxisAlignment mainAxisAlignment,\n        CrossAxisAlignment crossAxisAlignment,\n        List<? extends Widget> children\n    ) {\n        this.mainAxis = mainAxis;\n        this.mainAxisAlignment = mainAxisAlignment;\n        this.crossAxisAlignment = crossAxisAlignment;\n        this.children = children;\n    }\n\n    @Override\n    public WidgetState<MultiSplitPane> createState() {\n        return new MultiSplitPaneState();\n    }\n}\n\nclass MultiSplitPaneState extends WidgetState<MultiSplitPane> {\n\n    private List<Double> splits = null;\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new LayoutBuilder((innerContext, constraints) -> {\n            var axis = this.widget().mainAxis;\n            var children = this.widget().children;\n            var maxSize = constraints.maxOnAxis(axis) - ((children.size() - 2) * 2);\n\n            var splitSize = maxSize / (children.size());\n\n            if (this.splits == null) {\n                this.splits = new ArrayList<>();\n                for (int i = 0; i < children.size() - 1; i++) this.splits.add(splitSize * (i + 1));\n            }\n\n            var widgets = new ArrayList<Widget>();\n\n            for (int i = 0; i < children.size(); i++) {\n                var child = children.get(i);\n\n//                var split = MathHelper.clamp(this.splits.get(i), .1, .9) * maxSize;\n\n                var min = i == 0 ? 0 : this.splits.get(i - 1);\n                var max = i == children.size() - 1 ? maxSize : this.splits.get(i);\n\n                var childConstraints = Constraints.tight(axis.createSize(max - min, constraints.maxOnAxis(axis.opposite())));\n                widgets.add(new Constrain(childConstraints, child).key(child.key()));\n\n                if (i < children.size() - 1) {\n                    int finalI = i;\n                    widgets.add(new Flexible(\n                        new MouseArea(\n                            widget -> widget\n                                .dragCallback((x, y, dx, dy) -> setState(() -> {\n                                    this.splits.set(finalI,  this.splits.get(finalI) + axis.choose(dx, dy));\n                                }))\n                                .dragEndCallback(() -> {\n                                    this.splits.set(finalI, Mth.clamp(this.splits.get(finalI), .1 * maxSize, .9 * maxSize));\n                                })\n                                .cursorStyleSupplier((x, y) -> axis.choose(CursorStyle.HORIZONTAL_RESIZE, CursorStyle.VERTICAL_RESIZE)),\n                            new Box(Color.WHITE)\n                        )\n                    ).key(Key.of(\"splitter-\" + i)));\n                }\n            }\n\n            return new Flex(\n                axis,\n                this.widget().mainAxisAlignment,\n                this.widget().crossAxisAlignment,\n                null,\n                widgets\n            );\n\n\n//            var split = Math.floor(MathHelper.clamp(this.splitCoordinate, .1 * maxSize, .9 * maxSize));\n//\n//            var firstConstraints = Constraints.tight(axis.createSize(split, constraints.maxOnAxis(axis.opposite())));\n//            var secondConstraints = Constraints.tight(axis.createSize(maxSize - split, constraints.maxOnAxis(axis.opposite())));\n//\n//            return new Flex(\n//                axis,\n//                MainAxisAlignment.START,\n//                CrossAxisAlignment.START,\n//                new Constrain(firstConstraints, this.widget().firstChild).key(this.widget().firstChild.key()),\n//                new Flexible(\n//                    new MouseArea(\n//                        widget -> widget\n//                            .dragCallback((x, y, dx, dy) -> setState(() -> {\n//                                this.splitCoordinate = this.splitCoordinate + axis.choose(dx, dy);\n//                            }))\n//                            .dragEndCallback(() -> {\n//                                this.splitCoordinate = MathHelper.clamp(this.splitCoordinate, .1 * maxSize, .9 * maxSize);\n//                            })\n//                            .cursorStyleSupplier((x, y) -> axis.choose(CursorStyle.HORIZONTAL_RESIZE, CursorStyle.VERTICAL_RESIZE)),\n//                        new Box(Color.WHITE)\n//                    )\n//                ).key(Key.of(\"splitter\")),\n//                new Constrain(secondConstraints, this.widget().secondChild).key(this.widget().secondChild.key())\n//            );\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/splitpane/SplitPane.java",
    "content": "package io.wispforest.owo.braid.widgets.splitpane;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.Key;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Constrain;\nimport io.wispforest.owo.braid.widgets.basic.LayoutBuilder;\nimport io.wispforest.owo.braid.widgets.basic.MouseArea;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Flex;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport net.minecraft.util.Mth;\n\npublic class SplitPane extends StatefulWidget {\n\n    public final Widget firstChild;\n    public final Widget secondChild;\n    public final LayoutAxis axis;\n\n    public SplitPane(Widget firstChild, Widget secondChild, LayoutAxis axis) {\n        this.firstChild = firstChild;\n        this.secondChild = secondChild;\n        this.axis = axis;\n    }\n\n    @Override\n    public WidgetState<SplitPane> createState() {\n        return new SplitPaneState();\n    }\n}\n\nclass SplitPaneState extends WidgetState<SplitPane> {\n\n    private double splitCoordinate = -1;\n\n    @Override\n    public Widget build(BuildContext context) {\n        return new LayoutBuilder((innerContext, constraints) -> {\n            var axis = this.widget().axis;\n            var maxSize = constraints.maxOnAxis(axis) - 2;\n\n            if (this.splitCoordinate == -1) this.splitCoordinate = .5 * maxSize;\n            var split = Math.floor(Mth.clamp(this.splitCoordinate, .1 * maxSize, .9 * maxSize));\n\n            var firstConstraints = Constraints.tight(axis.createSize(split, constraints.maxOnAxis(axis.opposite())));\n            var secondConstraints = Constraints.tight(axis.createSize(maxSize - split, constraints.maxOnAxis(axis.opposite())));\n\n            return new Flex(\n                axis,\n                MainAxisAlignment.START,\n                CrossAxisAlignment.START,\n                new Constrain(firstConstraints, this.widget().firstChild).key(this.widget().firstChild.key()),\n                new Flexible(\n                    new MouseArea(\n                        widget -> widget\n                            .dragCallback((x, y, dx, dy) -> setState(() -> {\n                                this.splitCoordinate = this.splitCoordinate + axis.choose(dx, dy);\n                                System.out.println(\"Split coordinate: \" + this.splitCoordinate);\n                            }))\n                            .dragEndCallback(() -> {\n                                this.splitCoordinate = Mth.clamp(this.splitCoordinate, .1 * maxSize, .9 * maxSize);\n                            })\n                            .cursorStyleSupplier((x, y) -> axis.choose(CursorStyle.HORIZONTAL_RESIZE, CursorStyle.VERTICAL_RESIZE)),\n                        new Box(Color.WHITE)\n                    )\n                ).key(Key.of(\"splitter\")),\n                new Constrain(secondConstraints, this.widget().secondChild).key(this.widget().secondChild.key())\n            );\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/stack/Stack.java",
    "content": "package io.wispforest.owo.braid.widgets.stack;\n\nimport com.google.common.collect.Iterables;\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.BraidUtils;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;\nimport io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.OptionalDouble;\n\npublic class Stack extends MultiChildInstanceWidget {\n\n    public final Alignment alignment;\n\n    public Stack(Alignment alignment, List<? extends Widget> children) {\n        super(children);\n        this.alignment = alignment;\n    }\n\n    public Stack(List<? extends Widget> children) {\n        this(Alignment.CENTER, children);\n    }\n\n    public Stack(Alignment alignment, Widget... children) {\n        this(alignment, Arrays.asList(children));\n    }\n\n    public Stack(Widget... children) {\n        this(Alignment.CENTER, children);\n    }\n\n    @Override\n    public MultiChildWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends MultiChildWidgetInstance<Stack> {\n\n        public Instance(Stack widget) {\n            super(widget);\n        }\n\n        @Override\n        public void setWidget(Stack widget) {\n            if (this.widget.alignment == widget.alignment) return;\n\n            super.setWidget(widget);\n            this.markNeedsLayout();\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var sizingBase = this.children.stream().filter(child -> child.parentData == StackParentData.INSTANCE).findFirst().orElse(null);\n\n            Size selfSize;\n            if (sizingBase != null) {\n                selfSize = sizingBase.layout(constraints);\n\n                var childConstraints = Constraints.tight(selfSize);\n                for (var child : Iterables.filter(this.children, child -> child != sizingBase)) {\n                    child.layout(childConstraints);\n                }\n            } else {\n                selfSize = BraidUtils.fold(this.children, Size.zero(), (size, child) -> Size.max(size, child.layout(constraints)));\n            }\n\n            for (var child : this.children) {\n                child.transform.setX(\n                    this.widget.alignment.alignHorizontal(selfSize.width(), child.transform.width())\n                );\n                child.transform.setY(\n                    this.widget.alignment.alignVertical(selfSize.height(), child.transform.height())\n                );\n            }\n\n            this.transform.setSize(selfSize);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return BraidUtils.fold(\n                this.children,\n                0.0,\n                (width, child) -> Math.max(child.getIntrinsicWidth(height), width)\n            );\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return BraidUtils.fold(\n                this.children,\n                0.0,\n                (height, child) -> Math.max(child.getIntrinsicHeight(width), height)\n            );\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return this.computeHighestBaselineOffset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/stack/StackBase.java",
    "content": "package io.wispforest.owo.braid.widgets.stack;\n\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.VisitorWidget;\n\npublic class StackBase extends VisitorWidget {\n\n    public StackBase(Widget child) {\n        super(child);\n    }\n\n    private static final Visitor<StackBase> VISITOR = (widget, instance) -> {\n        if (instance.parentData != StackParentData.INSTANCE) {\n            instance.parentData = StackParentData.INSTANCE;\n            instance.markNeedsLayout();\n        }\n    };\n\n    @Override\n    public Proxy<?> proxy() {\n        return new VisitorWidget.Proxy<>(this, VISITOR);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/stack/StackParentData.java",
    "content": "package io.wispforest.owo.braid.widgets.stack;\n\npublic enum StackParentData {\n    INSTANCE;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/CopyTextIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic record CopyTextIntent(boolean delete) implements Intent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/DeleteLineIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic enum DeleteLineIntent implements Intent {\n    INSTANCE\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/DeleteTextIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic record DeleteTextIntent(boolean forwards, boolean entireWord) implements Intent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/EditableText.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.core.Aabb2d;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport io.wispforest.owo.braid.widgets.intents.Action;\nimport io.wispforest.owo.braid.widgets.intents.Actions;\nimport io.wispforest.owo.braid.widgets.intents.Intent;\nimport io.wispforest.owo.braid.widgets.scroll.ScrollAnimationSettings;\nimport io.wispforest.owo.braid.widgets.scroll.ScrollController;\nimport io.wispforest.owo.braid.widgets.scroll.Scrollable;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class EditableText extends StatefulWidget {\n\n    public final TextEditingController controller;\n    protected boolean softWrap = true;\n    protected boolean autoFocus = false;\n    protected List<TextInput.Formatter> formatters = new ArrayList<>();\n    protected Style baseStyle = Style.EMPTY;\n    protected Component suggestion = Component.empty();\n    protected boolean textShadow = false;\n    protected boolean suggestionIsPlaceholder = false;\n\n    public EditableText(\n        TextEditingController controller,\n        WidgetSetupCallback<EditableText> setupCallback\n    ) {\n        this.controller = controller;\n        setupCallback.setup(this);\n    }\n\n    public EditableText softWrap(boolean softWrap) {\n        this.assertMutable();\n        this.softWrap = softWrap;\n        return this;\n    }\n\n    public boolean softWrap() {\n        return this.softWrap;\n    }\n\n    public EditableText autoFocus(boolean autoFocus) {\n        this.assertMutable();\n        this.autoFocus = autoFocus;\n        return this;\n    }\n\n    public boolean autoFocus() {\n        return this.autoFocus;\n    }\n\n    public EditableText formatter(TextInput.Formatter formatter) {\n        this.assertMutable();\n        this.formatters.add(formatter);\n        return this;\n    }\n\n    public EditableText formatters(List<TextInput.Formatter> formatters) {\n        this.assertMutable();\n        this.formatters = formatters;\n        return this;\n    }\n\n    public List<TextInput.Formatter> formatters() {\n        return this.formatters;\n    }\n\n    public EditableText baseStyle(Style baseStyle) {\n        this.assertMutable();\n        this.baseStyle = baseStyle;\n        return this;\n    }\n\n    public Style baseStyle() {\n        return this.baseStyle;\n    }\n\n    public EditableText suggestion(Component suggestion) {\n        this.assertMutable();\n        this.suggestion = suggestion;\n        return this;\n    }\n\n    public Component suggestion() {\n        return this.suggestion;\n    }\n\n    public EditableText placeholder(Component placeholder) {\n        this.assertMutable();\n        this.suggestionIsPlaceholder = true;\n        return this.suggestion(placeholder);\n    }\n\n    public EditableText textShadow(boolean shadow) {\n        this.assertMutable();\n        this.textShadow = shadow;\n        return this;\n    }\n\n    public boolean textShadow() {\n        return this.textShadow;\n    }\n\n    public EditableText singleLine() {\n        return this\n            .softWrap(false)\n            .formatter(PatternFormatter.NO_NEWLINES);\n    }\n\n    @Override\n    public WidgetState<EditableText> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<EditableText> {\n\n        private final Runnable listener = this::listenerCallback;\n\n        private static final Duration CURSOR_BLINK_INTERVAL = Duration.ofMillis(300);\n        private boolean showCursor = false;\n        private boolean focused = false;\n\n        private long blinkCallbackId = -1;\n\n        private final ScrollController horizontalController = new ScrollController(this);\n        private final ScrollController verticalController = new ScrollController(this);\n        private BuildContext inputContext;\n\n        private final Map<Class<? extends Intent>, Action<?>> actions = new HashMap<>();\n\n        @Override\n        public void init() {\n            this.widget().controller.addListener(this.listener);\n\n            this.actions.put(\n                InsertNewlineIntent.class,\n                Action.<InsertNewlineIntent>callback((actionCtx, intent) -> this.instance().insert(\"\\n\"))\n            );\n\n            this.actions.put(\n                InsertTabIntent.class,\n                Action.<InsertTabIntent>callback((actionCtx, intent) -> this.instance().insert(\"  \"))\n            );\n\n            this.actions.put(\n                DeleteTextIntent.class,\n                Action.<DeleteTextIntent>callback((actionCtx, intent) -> this.instance().deleteText(intent))\n            );\n\n            this.actions.put(\n                DeleteLineIntent.class,\n                Action.<DeleteLineIntent>callback((actionCtx, intent) -> this.instance().deleteLine())\n            );\n\n            this.actions.put(\n                MoveCursorIntent.class,\n                Action.<MoveCursorIntent>callback((actionCtx, intent) -> this.instance().moveCursor(intent))\n            );\n\n            this.actions.put(\n                TeleportCursorIntent.class,\n                Action.<TeleportCursorIntent>callback((actionCtx, intent) -> this.instance().teleportCursor(intent))\n            );\n\n            this.actions.put(\n                SelectAllIntent.class,\n                Action.<SelectAllIntent>callback((actionCtx, intent) -> this.instance().selectAllText())\n            );\n\n            this.actions.put(\n                CopyTextIntent.class,\n                Action.<CopyTextIntent>callback((actionCtx, intent) -> this.instance().copyToClipboard(intent))\n            );\n\n            this.actions.put(\n                PasteTextIntent.class,\n                Action.<PasteTextIntent>callback((actionCtx, intent) -> this.instance().pasteFromClipboard())\n            );\n        }\n\n        @Override\n        public void didUpdateWidget(EditableText oldWidget) {\n            if (this.widget().controller != oldWidget.controller) {\n                oldWidget.controller.removeListener(this.listener);\n                this.widget().controller.addListener(this.listener);\n            }\n        }\n\n        @Override\n        public void dispose() {\n            this.widget().controller.removeListener(this.listener);\n        }\n\n        private void listenerCallback() {\n            this.schedulePostLayoutCallback(() -> {\n                var inputInstance = (TextInput.Instance) this.inputContext.instance();\n                var cursorPos = inputInstance.cursorPosition();\n                var lineHeight = inputInstance.host().client().font.lineHeight;\n\n                Scrollable.revealAabb(\n                    this.inputContext,\n                    new Aabb2d(\n                        cursorPos.x,\n                        cursorPos.y - lineHeight,\n                        2,\n                        lineHeight\n                    )\n                );\n            });\n\n            if (this.focused) {\n                this.restartBlinking();\n            }\n        }\n\n        private void restartBlinking() {\n            if (this.blinkCallbackId != -1) {\n                this.cancelDelayedCallback(this.blinkCallbackId);\n                this.blinkCallbackId = -1;\n            }\n\n            this.setState(() -> this.showCursor = true);\n\n            this.blinkCallbackId = this.scheduleDelayedCallback(CURSOR_BLINK_INTERVAL, this::blink);\n        }\n\n        private void stopBlinking() {\n            if (this.blinkCallbackId != -1) {\n                this.cancelDelayedCallback(this.blinkCallbackId);\n                this.blinkCallbackId = -1;\n            }\n\n            this.setState(() -> this.showCursor = false);\n        }\n\n        private void blink() {\n            this.setState(() -> this.showCursor = !this.showCursor);\n            this.blinkCallbackId = this.scheduleDelayedCallback(CURSOR_BLINK_INTERVAL, this::blink);\n        }\n\n        private TextInput.Instance instance() {\n            return (TextInput.Instance) this.inputContext.instance();\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = this.widget();\n            return new Focusable(\n                focusable -> focusable\n                    .focusGainedCallback(() -> {\n                        this.focused = true;\n                        this.restartBlinking();\n                    })\n                    .focusLostCallback(() -> {\n                        this.focused = false;\n                        this.stopBlinking();\n                    })\n                    .charCallback((charCode, modifiers) -> this.instance().onChar(charCode))\n                    .skipTraversal(true),\n                new Actions(\n                    actions -> actions\n                        .autoFocus(widget.autoFocus)\n                        .actions(this.actions),\n                    new Scrollable(\n                        true, true,\n                        this.horizontalController,\n                        this.verticalController,\n                        ScrollAnimationSettings.NO_ANIMATION,\n                        new Builder(inputContext -> {\n                            this.inputContext = inputContext;\n                            return new TextInput(\n                                widget.controller,\n                                this.showCursor,\n                                widget.softWrap,\n                                widget.formatters,\n                                widget.baseStyle,\n                                widget.textShadow,\n                                !widget.suggestionIsPlaceholder || widget.controller.value().text().isEmpty()\n                                    ? widget.suggestion\n                                    : Component.empty()\n                            );\n                        })\n                    )\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/InsertNewlineIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic enum InsertNewlineIntent implements Intent {\n    INSTANCE\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/InsertTabIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic enum InsertTabIntent implements Intent {\n    INSTANCE;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/MaxLengthFormatter.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\npublic record MaxLengthFormatter(int maxChars) implements TextInput.Formatter {\n\n    @Override\n    public TextEditingValue format(TextEditingValue previousState, TextEditingValue newState) {\n        if (newState.text().length() <= this.maxChars) {\n            return newState;\n        } else if (previousState.text().length() >= this.maxChars) {\n            return previousState;\n        }\n\n        return new TextEditingValue(\n            newState.text().substring(0, this.maxChars),\n            newState.selection().upper() > this.maxChars\n                ? TextSelection.collapsed(this.maxChars)\n                : newState.selection()\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/MoveCursorIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic record MoveCursorIntent(Direction direction, boolean skipWord, boolean selecting) implements Intent {\n    public enum Direction {\n        UP, DOWN, LEFT, RIGHT\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/PasteTextIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic enum PasteTextIntent implements Intent {\n    INSTANCE\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/PatternFormatter.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport net.minecraft.util.Mth;\n\nimport java.util.regex.Pattern;\n\npublic record PatternFormatter(Pattern pattern, String replacement, boolean allow) implements TextInput.Formatter {\n\n    public static PatternFormatter allow(Pattern pattern) {\n        return allow(pattern, \"\");\n    }\n\n    public static PatternFormatter allow(Pattern pattern, String replacement) {\n        return new PatternFormatter(pattern, replacement, true);\n    }\n\n    public static PatternFormatter deny(Pattern pattern) {\n        return deny(pattern, \"\");\n    }\n\n    public static PatternFormatter deny(Pattern pattern, String replacement) {\n        return new PatternFormatter(pattern, replacement, false);\n    }\n\n    @Override\n    public TextEditingValue format(TextEditingValue previousState, TextEditingValue newState) {\n        var state = new FormatState(newState);\n\n        var lastRegionEnd = 0;\n        for (var match : this.pattern.matcher(newState.text()).results().toList()) {\n            this.replaceRegion(lastRegionEnd, match.start(), this.allow, newState.text(), state);\n            this.replaceRegion(match.start(), match.end(), !this.allow, newState.text(), state);\n            lastRegionEnd = match.end();\n        }\n\n        this.replaceRegion(lastRegionEnd, newState.text().length(), this.allow, newState.text(), state);\n\n        return new TextEditingValue(\n            state.builder.toString(),\n            new TextSelection(\n                state.selectionStart,\n                state.selectionEnd\n            )\n        );\n    }\n\n    private void replaceRegion(int start, int end, boolean regionIsDenied, String input, FormatState state) {\n        var replacement = regionIsDenied\n            ? (start != end ? this.replacement : \"\")\n            : input.substring(start, end);\n\n        state.builder.append(replacement);\n\n        if (replacement.length() == end - start) {\n            return;\n        }\n\n        if (state.newValue.selection().start() > start) {\n            var startInRegion = Mth.clamp(state.newValue.selection().start(), start, end) - start;\n            state.selectionStart += replacement.length() - startInRegion;\n        }\n\n        if (state.newValue.selection().end() > start) {\n            var endInRegion = Mth.clamp(state.newValue.selection().end(), start, end) - start;\n            state.selectionEnd += replacement.length() - endInRegion;\n        }\n    }\n\n    private static final Pattern NEWLINE_PATTERN = Pattern.compile(\"\\n|\\r\\n\");\n    public static final PatternFormatter NO_NEWLINES = PatternFormatter.deny(NEWLINE_PATTERN);\n\n    private static class FormatState {\n        public final TextEditingValue newValue;\n\n        public final StringBuilder builder = new StringBuilder();\n        public int selectionStart;\n        public int selectionEnd;\n\n        public FormatState(TextEditingValue newValue) {\n            this.newValue = newValue;\n            this.selectionStart = newValue.selection().start();\n            this.selectionEnd = newValue.selection().end();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/SelectAllIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic enum SelectAllIntent implements Intent {\n    INSTANCE\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/TeleportCursorIntent.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.widgets.intents.Intent;\n\npublic record TeleportCursorIntent(boolean toStart, boolean selecting) implements Intent {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/TextBox.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.CommonColors;\n\npublic class TextBox extends StatefulWidget {\n\n    public final TextEditingController controller;\n    private final EditableText editableText;\n\n    public TextBox(\n        TextEditingController controller,\n        WidgetSetupCallback<EditableText> setupCallback\n    ) {\n        this.controller = controller;\n        this.editableText = new EditableText(\n            controller,\n            widget -> {\n                setupCallback.setup(widget);\n                widget.suggestion(widget.suggestion().copy().withStyle(style -> style.applyTo(Style.EMPTY.withColor(CommonColors.GRAY))));\n            }\n        );\n    }\n\n    @Override\n    public WidgetState<TextBox> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<TextBox> {\n\n        private boolean focused = false;\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Box(\n                //TODO: use panel instead of box here\n                this.focused ? Color.WHITE : new Color(CommonColors.LIGHT_GRAY),\n                new Focusable(\n                    widget -> widget\n                        .focusGainedCallback(() -> this.setState(() -> this.focused = true))\n                        .focusLostCallback(() -> this.setState(() -> this.focused = false))\n                        .skipTraversal(true),\n                    new Padding(\n                        Insets.all(1),\n                        new Box(\n                            Color.BLACK,\n                            new Padding(\n                                Insets.all(2),\n                                this.widget().editableText\n                            )\n                        )\n                    )\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/TextEditingController.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport io.wispforest.owo.braid.core.ListenableValue;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\n\npublic class TextEditingController extends ListenableValue<TextEditingValue> {\n\n    public TextEditingController(String text, TextSelection selection) {\n        super(new TextEditingValue(text, selection));\n    }\n\n    public TextEditingController(String text) {\n        this(text, TextSelection.collapsed(text.length()));\n    }\n\n    public TextEditingController() {\n        this(\"\");\n    }\n\n    public Component createTextForRendering(Style baseStyle) {\n        return Component.literal(this.value().text()).withStyle(style -> baseStyle.applyTo(baseStyle));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/TextEditingValue.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\npublic record TextEditingValue(String text, TextSelection selection) {\n    public TextEditingValue withText(String text) {\n        return new TextEditingValue(text, this.selection);\n    }\n\n    public TextEditingValue withSelection(TextSelection selection) {\n        return new TextEditingValue(this.text, selection);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/TextInput.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\nimport com.google.common.base.Preconditions;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.MouseListener;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.CommonColors;\nimport net.minecraft.util.FormattedCharSequence;\nimport net.minecraft.util.Mth;\nimport net.minecraft.util.StringUtil;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2d;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\n\npublic class TextInput extends LeafInstanceWidget {\n\n    public final TextEditingController controller;\n    public final boolean showCursor;\n    public final boolean softWrap;\n    public final List<Formatter> formatters;\n    public final Style baseStyle;\n    public final boolean textShadow;\n    public final Component suggestion;\n\n    public TextInput(TextEditingController controller, boolean showCursor, boolean softWrap, List<Formatter> formatters, Style baseStyle, boolean textShadow, @Nullable Component suggestion) {\n        this.controller = controller;\n        this.showCursor = showCursor;\n        this.softWrap = softWrap;\n        this.formatters = formatters;\n        this.baseStyle = baseStyle;\n        this.textShadow = textShadow;\n        this.suggestion = suggestion == null ? Component.empty() : suggestion;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    @FunctionalInterface\n    public interface Formatter {\n        TextEditingValue format(TextEditingValue previousState, TextEditingValue newState);\n    }\n\n    public static class Instance extends LeafWidgetInstance<TextInput> implements MouseListener {\n\n        protected TextEditingValue lastValue;\n        protected TextEditingValue value;\n\n        protected CursorLocation cursorLocation;\n\n        protected TextLayout.EditMetrics metrics = null;\n        protected List<FormattedCharSequence> renderLines = List.of();\n\n        public Instance(TextInput widget) {\n            super(widget);\n            this.lastValue = this.value = widget.controller.value();\n        }\n\n        public Vector2d cursorPosition() {\n            return this.coordinatesAtCharIdx(this.value.selection().end());\n        }\n\n        public TextLayout.LineMetrics currentLine() {\n            return this.metrics.lineMetrics().get(this.cursorLocation.line);\n        }\n\n        @Override\n        public void setWidget(TextInput widget) {\n            if (!(this.lastValue.equals(widget.controller.value())\n                && this.widget.softWrap == widget.softWrap\n                && this.widget.baseStyle.equals(widget.baseStyle)\n                && this.widget.suggestion.equals(widget.suggestion))) {\n\n                this.lastValue = this.value = widget.controller.value();\n\n                this.markNeedsLayout();\n            }\n\n            super.setWidget(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            var maxWidth = (int) (constraints.hasBoundedWidth() ? constraints.maxWidth() : constraints.minWidth());\n            var wrapWidth = this.widget.softWrap ? maxWidth - 2 : Integer.MAX_VALUE;\n\n            this.metrics = TextLayout.measure(\n                this.host().client().font,\n                this.value.text() + this.widget.suggestion.getString(),\n                this.widget.baseStyle,\n                wrapWidth\n            );\n\n            this.renderLines = new ArrayList<>(this.host().client().font.split(\n                this.widget.controller.createTextForRendering(this.widget.baseStyle).copy().append(this.widget.suggestion),\n                wrapWidth\n            ));\n\n            var size = Size.of(\n                this.metrics.width() + 1,\n                this.metrics.height()\n            ).constrained(constraints);\n\n            this.transform.setSize(size);\n\n            var newLineIdx = this.lineIdxAtCharIdx(this.value.selection().end());\n            this.cursorLocation = new CursorLocation(newLineIdx, this.value.selection().end() - this.metrics.lineMetrics().get(newLineIdx).beginIdx());\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return TextLayout.measure(\n                this.host().client().font,\n                this.value.text(),\n                this.widget.baseStyle,\n                Integer.MAX_VALUE\n            ).width();\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return TextLayout.measure(\n                this.host().client().font,\n                this.value.text(),\n                this.widget.baseStyle,\n                this.widget.softWrap ? (int) width : Integer.MAX_VALUE\n            ).height();\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.of(this.host().client().font.lineHeight - 2);\n        }\n\n        private void drawSelection(OwoUIGraphics ctx, double startX, double endX, double lineBaseY) {\n            var height = this.host().client().font.lineHeight;\n\n            ctx.push();\n            ctx.translate(startX, lineBaseY - height);\n\n            var width = endX - startX;\n            ctx.fill(RenderPipelines.GUI_TEXT_HIGHLIGHT, 0, 0, (int) width, height, CommonColors.BLUE);\n\n            ctx.pop();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            var font = this.host().client().font;\n\n            for (int lineIdx = 0; lineIdx < this.renderLines.size(); lineIdx++) {\n                graphics.drawString(\n                    font,\n                    this.renderLines.get(lineIdx),\n                    0,\n                    lineIdx * font.lineHeight,\n                    Color.WHITE.argb(),\n                    this.widget.textShadow\n                );\n            }\n\n            // ---\n\n            var selection = this.value.selection();\n            if (!selection.collapsed()) {\n                var startLine = this.lineIdxAtCharIdx(selection.lower());\n                var endLine = this.lineIdxAtCharIdx(selection.upper());\n\n                if (startLine == endLine) {\n                    var startPos = this.coordinatesAtCharIdx(selection.lower());\n                    var endPos = this.coordinatesAtCharIdx(selection.upper());\n\n                    this.drawSelection(graphics, startPos.x, endPos.x, endPos.y);\n                } else {\n                    var startPos = this.coordinatesAtCharIdx(selection.lower());\n                    this.drawSelection(graphics, startPos.x, this.metrics.lineMetrics().get(startLine).width(), startPos.y);\n\n                    for (var lineIdx = startLine + 1; lineIdx < endLine; lineIdx++) {\n                        var line = this.metrics.lineMetrics().get(lineIdx);\n                        var width = line.beginIdx() != line.endIdx() ? line.width() : 2;\n\n                        this.drawSelection(graphics, 0, width, (lineIdx + 1) * font.lineHeight);\n                    }\n\n                    var endPos = this.coordinatesAtCharIdx(selection.upper());\n                    drawSelection(graphics, 0, endPos.x, endPos.y);\n                }\n            }\n\n            // ---\n\n            if (this.widget.showCursor) {\n                var cursorPos = this.coordinatesAtCharIdx(this.value.selection().end());\n\n                graphics.vLine(\n                    (int) cursorPos.x,\n                    (int) (cursorPos.y - font.lineHeight - 2),\n                    (int) (cursorPos.y),\n                    0xaad0d0d0\n                );\n            }\n        }\n\n        private int lineIdxAtCharIdx(int charIdx) {\n            var matchedLineIdx = -1;\n            var lines = this.metrics.lineMetrics();\n\n            for (var lineIdx = 0; lineIdx < lines.size(); lineIdx++) {\n                var line = lines.get(lineIdx);\n                if (charIdx >= line.beginIdx() && charIdx <= line.endIdx()) {\n                    matchedLineIdx = lineIdx;\n\n                    break;\n                }\n            }\n\n            return matchedLineIdx != -1 ? matchedLineIdx : lines.size() - 1;\n        }\n\n        private Vector2d coordinatesAtCharIdx(int charIdx) {\n            var lineIdx = this.lineIdxAtCharIdx(charIdx);\n            var line = this.metrics.lineMetrics().get(lineIdx);\n\n            var font = this.host().client().font;\n            var text = this.value.text();\n\n            var x = font.width(text.substring(line.beginIdx(), Math.min(text.length(), charIdx)));\n            var y = (lineIdx + 1) * font.lineHeight;\n\n            return new Vector2d(x, y);\n        }\n\n        public void insert(String insertion) {\n            insertion = StringUtil.filterText(insertion, true);\n\n            var chars = new StringBuilder(this.value.text());\n            var selection = this.value.selection();\n            chars.replace(selection.lower(), selection.upper(), insertion);\n\n            var newText = chars.toString();\n            this.formatAndSetValue(new TextEditingValue(\n                newText,\n                TextSelection.collapsed(selection.lower() + insertion.length())\n            ));\n        }\n\n        private void deleteSelection() {\n            if (Owo.DEBUG) {\n                Preconditions.checkState(!this.value.selection().collapsed(), \"deleteSelection invoked with collapsed selection\");\n            }\n\n            this.insert(\"\");\n        }\n\n        private int lastTextLineIdx() {\n            var lastTextLineIdx = 0;\n            while (lastTextLineIdx < this.metrics.lineMetrics().size() && this.metrics.lineMetrics().get(lastTextLineIdx).endIdx() < this.value.text().length()) {\n                lastTextLineIdx++;\n            }\n\n            return lastTextLineIdx;\n        }\n\n        private void moveCursorVertically(int byLines, boolean selecting) {\n            var newLineIdx = Mth.clamp(this.cursorLocation.line + byLines, 0, this.lastTextLineIdx());\n            var currentX = this.cursorPosition().x;\n\n            var newLine = this.metrics.lineMetrics().get(newLineIdx);\n            var newLocalRune = 0;\n\n            var text = this.value.text();\n            var actualEndIdx = Math.min(newLine.endIdx(), text.length());\n            while (newLocalRune < (actualEndIdx - newLine.beginIdx())) {\n                var glyphX = this.host().client().font.width(text.substring(newLine.beginIdx(), newLine.beginIdx() + newLocalRune));\n\n                if (glyphX >= currentX) {\n                    var previousGlyphX = this.host().client().font.width(text.substring(newLine.beginIdx(), newLine.beginIdx() + Math.max(0, newLocalRune - 1)));\n\n                    if (Math.abs(currentX - previousGlyphX) < Math.abs(currentX - glyphX)) {\n                        newLocalRune--;\n                    }\n\n                    break;\n                }\n\n                newLocalRune++;\n            }\n\n            this.setCursorPosition(newLine.beginIdx() + newLocalRune, selecting);\n        }\n\n        private int charIdxAt(double x, double y) {\n            var font = this.host().client().font;\n\n            var clickedLine = this.metrics.lineMetrics().get(Mth.clamp((int) (y / font.lineHeight), 0, this.lastTextLineIdx()));\n            var lineText = this.value.text().substring(clickedLine.beginIdx(), Math.min(clickedLine.endIdx(), this.value.text().length()));\n\n            return clickedLine.beginIdx() + font.plainSubstrByWidth(lineText, (int) x + 1).length();\n        }\n\n        private void setCursorPosition(int toRune, boolean selecting) {\n            this.formatAndSetValue(this.value.withSelection(\n                selecting\n                    ? new TextSelection(this.value.selection().start(), toRune)\n                    : TextSelection.collapsed(toRune)\n            ));\n        }\n\n        private int nextWordBoundary(boolean forwards, OptionalInt fromChar) {\n            var fromCharIdx = fromChar.orElse(this.value.selection().end());\n\n            var direction = forwards ? 1 : -1;\n            var lookAhead = forwards ? 0 : -1;\n            var bound = forwards ? this.value.text().length() + 1 : -1;\n\n            var startingClass = SkipClass.of(this.safeCharAt(fromCharIdx + lookAhead));\n            var idx = fromCharIdx + direction;\n\n            while (idx != bound && startingClass.shouldSkip(this.safeCharAt(idx + lookAhead))) {\n                idx += direction;\n            }\n\n            return idx;\n        }\n\n        private char safeCharAt(int charIdx) {\n            var text = this.value.text();\n            return !text.isEmpty() ? text.charAt(Mth.clamp(charIdx, 0, text.length() - 1)) : ' ';\n        }\n\n        private void formatAndSetValue(TextEditingValue newValue) {\n            var actual = newValue;\n            if (!Objects.equals(this.value.text(), newValue.text())) {\n                actual = BraidUtils.fold(\n                    this.widget.formatters,\n                    newValue,\n                    (value, formatter) -> formatter.format(this.value, value)\n                );\n            }\n\n            this.widget.controller.setValue(this.value = actual);\n        }\n\n        // ---\n\n        public boolean onChar(int charCode) {\n            this.insert(Character.toString(charCode));\n            return true;\n        }\n\n        public void deleteText(DeleteTextIntent intent) {\n            var selection = this.value.selection();\n            if (!selection.collapsed()) {\n                this.deleteSelection();\n                return;\n            }\n\n            var text = this.value.text();\n            var cursorPosition = selection.end();\n\n            if (intent.forwards()) {\n                var chars = new StringBuilder(text);\n                var end = Math.min(\n                    text.length(),\n                    intent.entireWord()\n                        ? this.nextWordBoundary(true, OptionalInt.empty())\n                        : cursorPosition + 1\n                );\n\n                chars.delete(cursorPosition, end);\n\n                this.formatAndSetValue(new TextEditingValue(\n                    chars.toString(),\n                    TextSelection.collapsed(cursorPosition)\n                ));\n            } else {\n                var chars = new StringBuilder(text);\n                var start = Math.max(\n                    0,\n                    intent.entireWord()\n                        ? this.nextWordBoundary(false, OptionalInt.empty())\n                        : cursorPosition - 1\n                );\n                chars.delete(start, cursorPosition);\n\n                this.formatAndSetValue(new TextEditingValue(\n                    chars.toString(),\n                    TextSelection.collapsed(start)\n                ));\n            }\n        }\n\n        public void moveCursor(MoveCursorIntent intent) {\n            var selection = this.value.selection();\n            var text = this.value.text();\n            var cursorPosition = selection.end();\n\n            var endingSelection = !selection.collapsed() && !intent.selecting();\n\n            switch (intent.direction()) {\n                case UP -> this.moveCursorVertically(-1, intent.selecting());\n                case DOWN -> this.moveCursorVertically(1, intent.selecting());\n                case RIGHT -> this.setCursorPosition(\n                    Math.min(\n                        text.length(),\n                        endingSelection\n                            ? selection.upper()\n                            : intent.skipWord()\n                                ? this.nextWordBoundary(true, OptionalInt.empty())\n                                : cursorPosition + 1\n                    ),\n                    intent.selecting()\n                );\n                case LEFT -> this.setCursorPosition(\n                    Math.max(\n                        0,\n                        endingSelection\n                            ? selection.lower()\n                            : intent.skipWord()\n                                ? this.nextWordBoundary(false, OptionalInt.empty())\n                                : cursorPosition - 1\n                    ),\n                    intent.selecting()\n                );\n            }\n        }\n\n        public void pasteFromClipboard() {\n            this.insert(Minecraft.getInstance().keyboardHandler.getClipboard());\n        }\n\n        public void copyToClipboard(CopyTextIntent intent) {\n            Minecraft.getInstance().keyboardHandler.setClipboard(this.value.text().substring(\n                this.value.selection().lower(),\n                this.value.selection().upper()\n            ));\n\n            if (intent.delete()) {\n                this.deleteSelection();\n            }\n        }\n\n        public void selectAllText() {\n            this.formatAndSetValue(this.value.withSelection(new TextSelection(0, this.value.text().length())));\n        }\n\n        public void teleportCursor(TeleportCursorIntent intent) {\n            if (intent.toStart()) {\n                this.setCursorPosition(this.currentLine().beginIdx(), intent.selecting());\n            } else {\n                this.setCursorPosition(Math.min(this.value.text().length(), this.currentLine().endIdx()), intent.selecting());\n            }\n        }\n\n        public void deleteLine() {\n            var chars = new StringBuilder(this.value.text());\n            var line = this.currentLine();\n\n            chars.delete(line.beginIdx(), line.endIdx());\n            this.formatAndSetValue(new TextEditingValue(\n                chars.toString(),\n                TextSelection.collapsed(line.beginIdx())\n            ));\n        }\n\n        @Override\n        public @Nullable CursorStyle cursorStyleAt(double x, double y) {\n            return CursorStyle.TEXT;\n        }\n\n        private static final Duration MAX_DOUBLE_CLICK_DELAY = Duration.ofMillis(250);\n        private Instant lastClickTime = Instant.EPOCH;\n\n        @Override\n        public boolean onMouseDown(double x, double y, int button, KeyModifiers modifiers) {\n            var clickedIdx = this.charIdxAt(x, y);\n\n            if (Duration.between(this.lastClickTime, Instant.now()).compareTo(MAX_DOUBLE_CLICK_DELAY) < 0) {\n                var start = this.nextWordBoundary(false, OptionalInt.of(clickedIdx));\n                var end = this.nextWordBoundary(true, OptionalInt.of(clickedIdx));\n\n                this.formatAndSetValue(this.value.withSelection(\n                    new TextSelection(Math.max(0, start), end)\n                ));\n            } else {\n                this.lastClickTime = Instant.now();\n                this.setCursorPosition(clickedIdx, modifiers.shift());\n            }\n\n            return true;\n        }\n\n        @Override\n        public void onMouseDrag(double x, double y, double dx, double dy) {\n            this.setCursorPosition(this.charIdxAt(x, y), true);\n        }\n\n        protected interface SkipClass {\n            boolean shouldSkip(char c);\n\n            static SkipClass of(char c) {\n                if (c == '\\n') {\n                    return LineBreakClass.INSTANCE;\n                }\n\n                if (WordClass.isWordChar(c)) {\n                    return WordClass.INSTANCE;\n                }\n\n                return new NonWordClass(c);\n            }\n\n\n            enum WordClass implements SkipClass {\n                INSTANCE;\n\n                @Override\n                public boolean shouldSkip(char c) {\n                    return isWordChar(c);\n                }\n\n                public static boolean isWordChar(char c) {\n                    return c == '_' || Character.isAlphabetic(c) || Character.isDigit(c);\n                }\n            }\n\n            enum LineBreakClass implements SkipClass {\n                INSTANCE;\n\n                @Override\n                public boolean shouldSkip(char c) {\n                    return false;\n                }\n            }\n\n            record NonWordClass(char specimen) implements SkipClass {\n                @Override\n                public boolean shouldSkip(char c) {\n                    return c == this.specimen;\n                }\n            }\n        }\n\n        protected record CursorLocation(int line, int charIdx) {}\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/textinput/TextSelection.java",
    "content": "package io.wispforest.owo.braid.widgets.textinput;\n\npublic record TextSelection(int start, int end) {\n\n    public static TextSelection collapsed(int cursorPosition) {\n        return new TextSelection(cursorPosition, cursorPosition);\n    }\n\n    public int lower() {\n        return Math.min(this.start, this.end);\n    }\n\n    public int upper() {\n        return Math.max(this.start, this.end);\n    }\n\n    public boolean collapsed() {\n        return this.start == this.end;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/vanilla/VanillaWidget.java",
    "content": "package io.wispforest.owo.braid.widgets.vanilla;\n\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Builder;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport net.minecraft.client.gui.components.Renderable;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\n\nimport java.util.function.Supplier;\n\npublic class VanillaWidget<T extends Renderable & GuiEventListener> extends StatefulWidget {\n    //\"Yeah, I think this is a good name\" - glisco 2025\n\n    public final Size size;\n    public final Supplier<T> widgetSupplier;\n\n    public VanillaWidget(Size size, Supplier<T> widgetSupplier) {\n        this.widgetSupplier = widgetSupplier;\n        this.size = size;\n    }\n\n    @Override\n    public WidgetState<VanillaWidget<T>> createState() {\n        return new State<>();\n    }\n\n\n    //TODO: tell people they need to use a key in the case where they modify the supplier without modifying the tree, i dont like 100% understand this but glisco will know what this means\n    public static class State<T extends Renderable & GuiEventListener> extends WidgetState<VanillaWidget<T>> {\n\n        private T widget;\n        private BuildContext vanillaContext;\n\n        private VanillaWidgetWrapper.Instance instance() {\n            return (VanillaWidgetWrapper.Instance) this.vanillaContext.instance();\n        }\n\n        @Override\n        public void init() {\n            widget = this.widget().widgetSupplier.get();\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Center(\n                new Focusable(\n                    focusable -> focusable\n                        .keyDownCallback((keyCode, modifiers) -> this.instance().onKeyDown(keyCode, modifiers))\n                        .keyUpCallback((keyCode, modifiers) -> this.instance().onKeyUp(keyCode, modifiers))\n                        .charCallback((charCode, modifiers) -> this.instance().onChar(charCode, modifiers))\n                        .focusGainedCallback(() -> this.instance().onFocusGained())\n                        .focusLostCallback(() -> this.instance().onFocusLost()),\n                    new Sized(\n                        this.widget().size,\n                        new Builder(vanillaContext -> {\n                            this.vanillaContext = vanillaContext;\n                            return new VanillaWidgetWrapper<>(widget);\n                        })\n                    )\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/vanilla/VanillaWidgetWrapper.java",
    "content": "package io.wispforest.owo.braid.widgets.vanilla;\n\nimport com.mojang.blaze3d.opengl.GlStateManager;\nimport io.wispforest.owo.braid.core.BraidGraphics;\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.framework.instance.LeafWidgetInstance;\nimport io.wispforest.owo.braid.framework.instance.MouseListener;\nimport io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.components.Renderable;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.gui.layouts.LayoutElement;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.input.MouseButtonInfo;\n\nimport java.util.OptionalDouble;\n\npublic class VanillaWidgetWrapper<T extends Renderable & GuiEventListener> extends LeafInstanceWidget {\n\n    public final T wrapped;\n\n    public VanillaWidgetWrapper(T wrapped) {\n        this.wrapped = wrapped;\n    }\n\n    @Override\n    public LeafWidgetInstance<?> instantiate() {\n        return new Instance(this);\n    }\n\n    public static class Instance extends LeafWidgetInstance<VanillaWidgetWrapper<?>> implements MouseListener {\n        private int draggingMouseButton = 0;\n\n        private double x, y;\n\n        public Instance(VanillaWidgetWrapper<?> widget) {\n            super(widget);\n        }\n\n        @Override\n        protected void doLayout(Constraints constraints) {\n            if (widget.wrapped instanceof LayoutElement layoutElement) {\n                layoutElement.setPosition(0, 0);\n            }\n\n            var size = constraints.hasBoundedWidth() && constraints.hasBoundedHeight()\n                ? constraints.maxSize()\n                : constraints.minSize();\n\n            if (widget.wrapped instanceof AbstractWidget abstractWidget) {\n                abstractWidget.setWidth((int) size.width());\n                abstractWidget.setHeight((int) size.height());\n            }\n\n            this.transform.setSize(size);\n        }\n\n        @Override\n        protected double measureIntrinsicWidth(double height) {\n            return 0;\n        }\n\n        @Override\n        protected double measureIntrinsicHeight(double width) {\n            return 0;\n        }\n\n        @Override\n        protected OptionalDouble measureBaselineOffset() {\n            return OptionalDouble.empty();\n        }\n\n        @Override\n        public void draw(BraidGraphics graphics) {\n            widget.wrapped.render(graphics, (int) x, (int) y, host().client().getDeltaTracker().getGameTimeDeltaPartialTick(false));\n\n            GlStateManager._enableScissorTest();\n        }\n\n        public boolean onKeyDown(int keyCode, KeyModifiers modifiers) {\n            return widget.wrapped.keyPressed(new KeyEvent(keyCode, 0, modifiers.bitMask()));\n        }\n\n        public boolean onKeyUp(int keyCode, KeyModifiers modifiers) {\n            return widget.wrapped.keyReleased(new KeyEvent(keyCode, 0, modifiers.bitMask()));\n        }\n\n        public boolean onChar(int charCode, KeyModifiers modifiers) {\n            return widget.wrapped.charTyped(new CharacterEvent(charCode, modifiers.bitMask()));\n        }\n\n        public void onFocusGained() {\n            this.widget.wrapped.setFocused(true);\n        }\n\n        public void onFocusLost() {\n            this.widget.wrapped.setFocused(false);\n        }\n\n        @Override\n        public boolean onMouseDown(double x, double y, int button, KeyModifiers modifiers) {\n            return widget.wrapped.mouseClicked(new MouseButtonEvent(x, y, new MouseButtonInfo(button, modifiers.bitMask())), false);\n        }\n\n        @Override\n        public boolean onMouseUp(double x, double y, int button, KeyModifiers modifiers) {\n            return widget.wrapped.mouseReleased(new MouseButtonEvent(x, y, new MouseButtonInfo(button, modifiers.bitMask())));\n        }\n\n        @Override\n        public void onMouseMove(double toX, double toY) {\n            this.x = toX;\n            this.y = toY;\n        }\n\n        @Override\n        public void onMouseDragStart(int button, KeyModifiers modifiers) {\n            draggingMouseButton = button;\n        }\n\n        @Override\n        public void onMouseDrag(double x, double y, double dx, double dy) {\n            this.widget.wrapped.mouseDragged(new MouseButtonEvent(x, y, new MouseButtonInfo(draggingMouseButton, 0)), (int) dx, (int) dy);\n        }\n\n        @Override\n        public boolean onMouseScroll(double x, double y, double horizontal, double vertical) {\n            return widget.wrapped.mouseScrolled(x, y, horizontal, vertical);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/window/Window.java",
    "content": "package io.wispforest.owo.braid.widgets.window;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.HoverStyledLabel;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.drag.DragArenaElement;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.intents.Interactable;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class Window extends StatefulWidget {\n\n    public final boolean collapsible;\n    public final Component title;\n    public final @Nullable Runnable onClose;\n    public final @Nullable WindowController controller;\n    public final Size initialSize;\n    public final Size minSize;\n    public final Size maxSize;\n\n    public final Widget content;\n\n    public Window(boolean collapsible, Component title, @Nullable Runnable onClose, @Nullable WindowController controller, Size initialSize, Size minSize, Size maxSize, Widget content) {\n        this.collapsible = collapsible;\n        this.title = title;\n        this.onClose = onClose;\n        this.controller = controller;\n        this.initialSize = initialSize;\n        this.minSize = minSize;\n        this.maxSize = maxSize;\n        this.content = content;\n    }\n\n    public Window(boolean collapsible, Component title, @Nullable Runnable onClose, @Nullable WindowController controller, Size initialSize, Widget content) {\n        this(collapsible, title, onClose, controller, initialSize, Size.square(40), Size.square(Double.POSITIVE_INFINITY), content);\n    }\n\n    @Override\n    public WidgetState<Window> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<Window> {\n\n        private WindowController controller;\n        private WindowController internalController;\n\n        private Set<Edge> draggingEdges;\n        private Size draggingSize;\n\n        @Override\n        public void init() {\n            this.internalController = new WindowController();\n            this.updateController();\n\n            this.controller.setSize(this.widget().initialSize);\n            this.applySize(this.widget().initialSize);\n        }\n\n        @Override\n        public void didUpdateWidget(Window oldWidget) {\n            this.updateController();\n            this.applySize(this.controller.size());\n        }\n\n        private void updateController() {\n            this.controller = this.widget().controller != null ? this.widget().controller : this.internalController;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new ListenableBuilder(\n                this.controller,\n                (buildContext, child) -> {\n                    var titleBar = new ArrayList<Widget>();\n                    if (this.widget().collapsible) {\n                        titleBar.add(Interactable.primary(\n                            () -> this.controller.toggleCollapsed(),\n                            new Padding(\n                                Insets.of(2, 0, 0, 4),\n                                new Label(Component.literal(this.controller.collapsed() ? \"⏶\" : \"⏷\"))\n                            )\n                        ));\n                    }\n\n                    titleBar.add(new Flexible(new Label(new LabelStyle(Alignment.LEFT, null, null, null), false, Label.Overflow.ELLIPSIS, this.widget().title)));\n\n                    if (this.widget().onClose != null) {\n                        titleBar.add(Interactable.primary(\n                            () -> this.widget().onClose.run(),\n                            new HoverStyledLabel(Component.literal(\"x\"), Style.EMPTY.applyFormat(ChatFormatting.RED))\n                        ));\n                    }\n\n                    return new DragArenaElement(\n                        Math.ceil(this.controller.x()),\n                        Math.ceil(this.controller.y()),\n                        new MouseArea(\n                            widget -> widget\n                                //TODO: decide what to do with buttons here\n                                .clickCallback((x, y, button, modifiers) -> {\n                                    if (button != 0) return false;\n                                    this.draggingEdges = this.edgesAt(x, y);\n                                    this.draggingSize = this.controller.size();\n                                    return true;\n                                })\n                                .dragCallback((x, y, dx, dy) -> this.resize(dx, dy))\n                                .dragEndCallback(() -> {\n                                    this.draggingEdges = null;\n                                    this.draggingSize = null;\n                                })\n                                .cursorStyleSupplier((x, y) -> this.cursorStyleFor(this.edgesAt(x, y))),\n                            new Padding(\n                                Insets.all(4),\n                                new HitTestTrap(\n                                    new MouseArea(\n                                        widget -> widget\n                                            .dragCallback((x, y, dx, dy) -> {\n                                                this.controller.setX(this.controller.x() + dx);\n                                                this.controller.setY(this.controller.y() + dy);\n                                            }),\n                                        new Sized(\n                                            Size.of(\n                                                this.controller.size().width(),\n                                                this.controller.size().height() + 15\n                                            ).floor(),\n                                            new Column(\n                                                new Sized(\n                                                    null,\n                                                    15.0,\n                                                    new Box(\n                                                        Color.BLACK.withA(.75),\n                                                        new Padding(\n                                                            Insets.horizontal(4),\n                                                            new Row(titleBar)\n                                                        )\n                                                    )\n                                                ),\n                                                new Flexible(\n                                                    new Visibility(\n                                                        !this.controller.collapsed(),\n                                                        false,\n                                                        child\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    );\n                },\n                new Box(\n                    Color.BLACK.withA(.65),\n                    new Padding(\n                        Insets.all(4),\n                        new Clip(\n                            this.widget().content\n                        )\n                    )\n                )\n            );\n        }\n\n        protected Set<Edge> edgesAt(double x, double y) {\n            var result = new HashSet<Edge>();\n\n            if (y < 4) result.add(Edge.TOP);\n            if (y > this.controller.size().height() + 4 + 15) result.add(Edge.BOTTOM);\n\n            if (x < 4) result.add(Edge.LEFT);\n            if (x > this.controller.size().width() + 4) result.add(Edge.RIGHT);\n\n            return result;\n        }\n\n        protected void resize(double dx, double dy) {\n            var size = this.draggingSize;\n\n            if (this.draggingEdges.contains(Edge.TOP)) {\n                size = size.with(null, size.height() - dy);\n                this.controller.setY(this.controller.y() + dy);\n            } else if (this.draggingEdges.contains(Edge.BOTTOM)) {\n                size = size.with(null, size.height() + dy);\n            }\n\n            if (this.draggingEdges.contains(Edge.LEFT)) {\n                size = size.with(size.width() - dx, null);\n                this.controller.setX(this.controller.x() + dx);\n            } else if (this.draggingEdges.contains(Edge.RIGHT)) {\n                size = size.with(size.width() + dx, null);\n            }\n\n            this.draggingSize = size;\n            this.applySize(this.draggingSize);\n        }\n\n        private void applySize(Size size) {\n            this.controller.setSize(Size.of(\n                Mth.clamp(size.width(), this.widget().minSize.width(), this.widget().maxSize.width()),\n                Mth.clamp(size.height(), this.widget().minSize.height(), this.widget().maxSize.height())\n            ));\n        }\n\n        protected @Nullable CursorStyle cursorStyleFor(Set<Edge> edges) {\n            if (edges.size() == 1) {\n                if (edges.contains(Edge.TOP) || edges.contains(Edge.BOTTOM)) return CursorStyle.VERTICAL_RESIZE;\n                if (edges.contains(Edge.LEFT) || edges.contains(Edge.RIGHT)) return CursorStyle.HORIZONTAL_RESIZE;\n            } else if (edges.size() == 2) {\n                if ((edges.contains(Edge.TOP) && edges.contains(Edge.LEFT)) || (edges.contains(Edge.BOTTOM) && edges.contains(Edge.RIGHT))) {\n                    return CursorStyle.NWSE_RESIZE;\n                }\n\n                if ((edges.contains(Edge.BOTTOM) && edges.contains(Edge.LEFT)) || (edges.contains(Edge.TOP) && edges.contains(Edge.RIGHT))) {\n                    return CursorStyle.NESW_RESIZE;\n                }\n            }\n\n            return null;\n        }\n    }\n\n    protected enum Edge {\n        TOP, LEFT, RIGHT, BOTTOM\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/braid/widgets/window/WindowController.java",
    "content": "package io.wispforest.owo.braid.widgets.window;\n\nimport io.wispforest.owo.braid.core.Listenable;\nimport io.wispforest.owo.braid.core.Size;\n\npublic class WindowController extends Listenable {\n    private double x = 0;\n    private double y = 0;\n    private Size size = Size.zero();\n\n    private boolean collapsed = false;\n\n    public void setX(double x) {\n        this.x = x;\n        this.notifyListeners();\n    }\n\n    public double x() {\n        return this.x;\n    }\n\n    public void setY(double y) {\n        this.y = y;\n        this.notifyListeners();\n    }\n\n    public double y() {\n        return this.y;\n    }\n\n    public void setSize(Size size) {\n        this.size = size;\n        this.notifyListeners();\n    }\n\n    public Size size() {\n        return this.size;\n    }\n\n    public boolean toggleCollapsed() {\n        this.setCollapsed(!this.collapsed);\n        return this.collapsed;\n    }\n\n    public void setCollapsed(boolean collapsed) {\n        this.collapsed = collapsed;\n        this.notifyListeners();\n    }\n\n    public boolean collapsed() {\n        return this.collapsed;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/OwoClient.java",
    "content": "package io.wispforest.owo.client;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.display.BraidDisplay;\nimport io.wispforest.owo.client.screens.MenuNetworkingInternals;\nimport io.wispforest.owo.command.debug.OwoDebugCommands;\nimport io.wispforest.owo.config.OwoConfigCommand;\nimport io.wispforest.owo.itemgroup.json.OwoItemGroupLoader;\nimport io.wispforest.owo.moddata.ModDataLoader;\nimport io.wispforest.owo.ui.core.OwoUIPipelines;\nimport io.wispforest.owo.ui.parsing.UIModelLoader;\nimport io.wispforest.owo.ui.renderstate.OwoSpecialGuiElementRenderers;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport net.fabricmc.api.ClientModInitializer;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;\nimport net.fabricmc.fabric.api.resource.ResourceManagerHelper;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.server.packs.PackType;\nimport net.minecraft.util.Util;\nimport org.jetbrains.annotations.ApiStatus;\n\n@ApiStatus.Internal\n@Environment(EnvType.CLIENT)\npublic class OwoClient implements ClientModInitializer {\n\n    private static final String LINUX_RENDERDOC_WARNING = \"\"\"\n        \n        ========================================\n        Ignored 'owo.renderdocPath' property as this Minecraft instance is not running on Windows.\n        Please populate the LD_PRELOAD environment variable instead\n        ========================================\"\"\";\n\n    private static final String MAC_RENDERDOC_WARNING = \"\"\"\n        \n        ========================================\n        Ignored 'owo.renderdocPath' property as this Minecraft instance is not running on Windows.\n        RenderDoc is not supported on macOS\n        ========================================\"\"\";\n\n    private static final String GENERIC_RENDERDOC_WARNING = \"\"\"\n        \n        ========================================\n        Ignored 'owo.renderdocPath' property as this Minecraft instance is not running on Windows.\n        ========================================\"\"\";\n\n    @Override\n    public void onInitializeClient() {\n        ModDataLoader.load(OwoItemGroupLoader.INSTANCE);\n\n        ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new UIModelLoader());\n        ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new NinePatchTexture.MetadataLoader());\n\n        OwoUIPipelines.register();\n        RenderPipelines.register(BraidDisplay.PIPELINE);\n\n        final var renderdocPath = System.getProperty(\"owo.renderdocPath\");\n        if (renderdocPath != null) {\n            if (Util.getPlatform() == Util.OS.WINDOWS) {\n                System.load(renderdocPath);\n            } else {\n                Owo.LOGGER.warn(switch (Util.getPlatform()) {\n                    case LINUX -> LINUX_RENDERDOC_WARNING;\n                    case OSX -> MAC_RENDERDOC_WARNING;\n                    default -> GENERIC_RENDERDOC_WARNING;\n                });\n            }\n        }\n\n        MenuNetworkingInternals.Client.init();\n\n        ClientCommandRegistrationCallback.EVENT.register(OwoConfigCommand::register);\n\n        if (Owo.DEBUG) {\n            OwoDebugCommands.Client.register();\n        }\n\n        OwoSpecialGuiElementRenderers.init();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/screens/MenuNetworkingInternals.java",
    "content": "package io.wispforest.owo.client.screens;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.util.pond.OwoAbstractContainerMenuExtension;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;\nimport net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;\nimport net.minecraft.client.gui.screens.inventory.MenuAccess;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.ApiStatus;\n\n@ApiStatus.Internal\npublic class MenuNetworkingInternals {\n    public static final Identifier SYNC_PROPERTIES = Owo.id(\"sync_menu_properties\");\n\n    public static void init() {\n        var localPacketCodec = CodecUtils.toPacketCodec(LocalPacket.ENDEC);\n\n        PayloadTypeRegistry.playS2C().register(LocalPacket.ID, localPacketCodec);\n        PayloadTypeRegistry.playC2S().register(LocalPacket.ID, localPacketCodec);\n        PayloadTypeRegistry.playS2C().register(SyncPropertiesPacket.ID, CodecUtils.toPacketCodec(SyncPropertiesPacket.ENDEC));\n\n        ServerPlayNetworking.registerGlobalReceiver(LocalPacket.ID, (payload, context) -> {\n            var menu = context.player().containerMenu;\n\n            if (menu == null) {\n                Owo.LOGGER.error(\"Received local packet for null ContainerMenu\");\n                return;\n            }\n\n            ((OwoAbstractContainerMenuExtension) menu).owo$handlePacket(payload, false);\n        });\n    }\n\n    public record LocalPacket(int packetId, FriendlyByteBuf payload) implements CustomPacketPayload {\n        public static final Type<LocalPacket> ID = new Type<>(Owo.id(\"local_packet\"));\n        public static final Endec<LocalPacket> ENDEC = StructEndecBuilder.of(\n            Endec.VAR_INT.fieldOf(\"packetId\", LocalPacket::packetId),\n            MinecraftEndecs.FRIENDLY_BYTE_BUF.fieldOf(\"payload\", LocalPacket::payload),\n            LocalPacket::new\n        );\n\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return ID;\n        }\n    }\n\n    public record SyncPropertiesPacket(FriendlyByteBuf payload) implements CustomPacketPayload {\n        public static final Type<SyncPropertiesPacket> ID = new Type<>(SYNC_PROPERTIES);\n        public static final Endec<SyncPropertiesPacket> ENDEC = StructEndecBuilder.of(\n            MinecraftEndecs.FRIENDLY_BYTE_BUF.fieldOf(\"payload\", SyncPropertiesPacket::payload),\n            SyncPropertiesPacket::new\n        );\n\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return ID;\n        }\n    }\n\n    @Environment(EnvType.CLIENT)\n    public static class Client {\n        public static void init() {\n            ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {\n                if (screen instanceof MenuAccess<?> handled)\n                    ((OwoAbstractContainerMenuExtension) handled.getMenu()).owo$attachToPlayer(client.player);\n            });\n\n            ClientPlayNetworking.registerGlobalReceiver(LocalPacket.ID, (payload, context) -> {\n                var menu = context.player().containerMenu;\n\n                if (menu == null) {\n                    Owo.LOGGER.error(\"Received local packet for null ContainerMenu\");\n                    return;\n                }\n\n                ((OwoAbstractContainerMenuExtension) menu).owo$handlePacket(payload, true);\n            });\n\n            ClientPlayNetworking.registerGlobalReceiver(SyncPropertiesPacket.ID, (payload, context) -> {\n                var menu = context.player().containerMenu;\n\n                if (menu == null) {\n                    Owo.LOGGER.error(\"Received sync properties packet for null ContainerMenu\");\n                    return;\n                }\n\n                ((OwoAbstractContainerMenuExtension) menu).owo$readPropertySync(payload);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/screens/MenuUtils.java",
    "content": "package io.wispforest.owo.client.screens;\n\nimport io.wispforest.owo.mixin.AbstractContainerMenuInvoker;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.item.ItemStack;\n\n/**\n * A collection of utilities to ease implementing a simple {@link net.minecraft.client.gui.screens.inventory.AbstractContainerScreen}\n */\npublic class MenuUtils {\n\n    /**\n     * Can be used as an implementation of {@link net.minecraft.world.inventory.AbstractContainerMenu#quickMoveStack(Player, int)}\n     * for simple screens with a lower (player) and upper (main) inventory\n     *\n     * <pre>\n     * {@code\n     * @Override\n     * public ItemStack quickMove(PlayerEntity player, int invSlot) {\n     *     return MenuUtils.handleSlotTransfer(this, invSlot, this.inventory.size());\n     * }\n     * }\n     * </pre>\n     *\n     * @param menu               The target AbstractContainerMenu\n     * @param clickedSlotIndex   The slot index that was clicked\n     * @param upperInventorySize The size of the upper (main) inventory\n     * @return The return value for {{@link net.minecraft.world.inventory.AbstractContainerMenu#quickMoveStack(Player, int)}}\n     */\n    public static ItemStack handleSlotTransfer(AbstractContainerMenu menu, int clickedSlotIndex, int upperInventorySize) {\n        final var slots = menu.slots;\n        final var clickedSlot = slots.get(clickedSlotIndex);\n        if (!clickedSlot.hasItem()) return ItemStack.EMPTY;\n\n        final var clickedStack = clickedSlot.getItem();\n\n        if (clickedSlotIndex < upperInventorySize) {\n            if (!insertIntoSlotRange(menu, clickedStack, upperInventorySize, slots.size(), true)) {\n                return ItemStack.EMPTY;\n            }\n        } else {\n            if (!insertIntoSlotRange(menu, clickedStack, 0, upperInventorySize)) {\n                return ItemStack.EMPTY;\n            }\n        }\n\n        if (clickedStack.isEmpty()) {\n            clickedSlot.setByPlayer(ItemStack.EMPTY);\n        } else {\n            clickedSlot.setChanged();\n        }\n\n        return clickedStack;\n    }\n\n    /**\n     * Shorthand of {@link #insertIntoSlotRange(AbstractContainerMenu, ItemStack, int, int, boolean)} with\n     * {@code false} for {@code fromLast}\n     */\n    @SuppressWarnings(\"BooleanMethodIsAlwaysInverted\")\n    public static boolean insertIntoSlotRange(AbstractContainerMenu menu, ItemStack addition, int beginIndex, int endIndex) {\n        return insertIntoSlotRange(menu, addition, beginIndex, endIndex, false);\n    }\n\n    /**\n     * Tries to insert the {@code addition} stack into all slots in the given range\n     *\n     * @param menu       The AbstractContainerMenu to operate on\n     * @param beginIndex The index of the first slot to check\n     * @param endIndex   The index of the last slot to check\n     * @param addition   The ItemStack to try and insert, this gets mutated\n     *                   if insertion (partly) succeeds\n     * @param fromLast   If {@code true}, iterate the range of slots in\n     *                   opposite order\n     * @return {@code true} if state was modified\n     */\n    @SuppressWarnings(\"BooleanMethodIsAlwaysInverted\")\n    public static boolean insertIntoSlotRange(AbstractContainerMenu menu, ItemStack addition, int beginIndex, int endIndex, boolean fromLast) {\n        return ((AbstractContainerMenuInvoker) menu).owo$insertItem(addition, beginIndex, endIndex, fromLast);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/screens/OwoAbstractContainerMenu.java",
    "content": "package io.wispforest.owo.client.screens;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport net.minecraft.world.entity.player.Player;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Consumer;\n\npublic interface OwoAbstractContainerMenu {\n\n    default ReflectiveEndecBuilder endecBuilder() {\n        throw new UnsupportedOperationException(\"Implemented in AbstractContainerMenuMixin\");\n    }\n\n    /**\n     * Create a new property on this menu. This property can be updated serverside\n     * and will automatically synchronize to the client - think {@link net.minecraft.world.inventory.ContainerData}\n     * but without being restricted to integers\n     *\n     * @param clazz   The class of the property's value\n     * @param endec   The endec to use for (de-)serializing the value of this property over the network\n     * @param initial The value with which to initialize the property\n     * @return The created property\n     */\n    default <T> SyncedProperty<T> createProperty(Class<T> clazz, Endec<T> endec, T initial) {\n        throw new UnsupportedOperationException(\"Implemented in AbstractContainerMenuMixin\");\n    }\n\n    /**\n     * Shorthand for {@link #createProperty(Class, Endec, Object)} which creates the endec\n     * through {@link ReflectiveEndecBuilder#get(Class)}\n     */\n    default <T> SyncedProperty<T> createProperty(Class<T> clazz, T initial) {\n        return this.createProperty(clazz, this.endecBuilder().get(clazz), initial);\n    }\n\n    /**\n     * Register a serverbound message, or local packet if you will, onto this\n     * menu. This needs to be called during initialization of the menu,\n     * after which you can send messages to the server by invoking {@link #sendMessage(Record)}\n     * with the message you want to send\n     *\n     * @param messageClass The class of message to send, must be a record - much like\n     *                     packets in an {@link io.wispforest.owo.network.OwoNetChannel}\n     * @param endec        The endec to use for (de-)serializing messages sent over the network\n     * @param handler      The handler to execute when a message of the given class is\n     *                     received on the server\n     */\n    default <R extends Record> void addServerboundMessage(Class<R> messageClass, Endec<R> endec, Consumer<R> handler) {\n        throw new UnsupportedOperationException(\"Implemented in AbstractContainerMenuMixin\");\n    }\n\n    /**\n     * Shorthand for {@link #addServerboundMessage(Class, Endec, Consumer)} which creates the endec\n     * through {@link ReflectiveEndecBuilder#get(Class)}\n     */\n    default <R extends Record> void addServerboundMessage(Class<R> messageClass, Consumer<R> handler) {\n        this.addServerboundMessage(messageClass, this.endecBuilder().get(messageClass), handler);\n    }\n\n    /**\n     * Register a clientbound message, or local packet if you will, onto this\n     * menu. This needs to be called during initialization of the menu,\n     * after which you can send messages to the client by invoking {@link #sendMessage(Record)}\n     * with the message you want to send\n     *\n     * @param messageClass The class of message to send, must be a record - much like\n     *                     packets in an {@link io.wispforest.owo.network.OwoNetChannel}\n     * @param endec        The endec to use for (de-)serializing messages sent over the network\n     * @param handler      The handler to execute when a message of the given class is\n     *                     received on the client\n     */\n    default <R extends Record> void addClientboundMessage(Class<R> messageClass, Endec<R> endec, Consumer<R> handler) {\n        throw new UnsupportedOperationException(\"Implemented in AbstractContainerMenuMixin\");\n    }\n\n    /**\n     * Shorthand for {@link #addClientboundMessage(Class, Endec, Consumer)} which creates the endec\n     * through {@link ReflectiveEndecBuilder#get(Class)}\n     */\n    default <R extends Record> void addClientboundMessage(Class<R> messageClass, Consumer<R> handler) {\n        this.addClientboundMessage(messageClass, this.endecBuilder().get(messageClass), handler);\n    }\n\n    /**\n     * Send the given message. This message must have been previously\n     * registered through a call to {@link #addServerboundMessage(Class, Endec, Consumer)}\n     * or {@link #addClientboundMessage(Class, Endec, Consumer)} - this also dictates where\n     * the message will be sent to\n     */\n    default <R extends Record> void sendMessage(@NotNull R message) {\n        throw new UnsupportedOperationException(\"Implemented in AbstractContainerMenuMixin\");\n    }\n\n    /**\n     * @return The player this menu is attached to\n     */\n    default Player player() {\n        throw new UnsupportedOperationException(\"Implemented in AbstractContainerMenuMixin\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/screens/ScreenhandlerMessageData.java",
    "content": "package io.wispforest.owo.client.screens;\n\nimport io.wispforest.endec.Endec;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.function.Consumer;\n\n@ApiStatus.Internal\npublic record ScreenhandlerMessageData<T>(int id, boolean clientbound, Endec<T> endec, Consumer<T> handler) {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/screens/SlotGenerator.java",
    "content": "package io.wispforest.owo.client.screens;\n\nimport net.minecraft.world.Container;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.inventory.Slot;\n\nimport java.util.function.Consumer;\n\n/**\n * Stateful slot generation utility for easily\n * arranging the slot grid used in a {@link net.minecraft.world.inventory.AbstractContainerMenu}\n */\npublic final class SlotGenerator {\n\n    private int anchorX, anchorY;\n    private int horizontalSpacing = 0;\n    private int verticalSpacing = 0;\n\n    private SlotFactory slotFactory = Slot::new;\n    private Consumer<Slot> slotConsumer;\n\n    private SlotGenerator(Consumer<Slot> slotConsumer, int anchorX, int anchorY) {\n        this.anchorX = anchorX;\n        this.anchorY = anchorY;\n        this.slotConsumer = slotConsumer;\n    }\n\n    /**\n     * Begin generating slots into {@code slotConsumer}, starting at\n     * ({@code anchorX}, {@code anchorY}). Usually, the {@code slotConsumer}\n     * will be the {@code addSlot} method of the screen handler for which\n     * slots are being generated\n     * <p>\n     * <pre>\n     * {@code\n     * SlotGenerator.begin(this::addSlot, 50, 10)\n     *     .grid(someInventory, 0, 3, 3) // add a 3x3 grid of slots 0-8 of 'someInventory'\n     *     .moveTo(10, 100)\n     *     .playerInventory(playerInventory); // add the player inventory and hotbar slots\n     * }\n     * </pre>\n     */\n    public static SlotGenerator begin(Consumer<Slot> slotConsumer, int anchorX, int anchorY) {\n        return new SlotGenerator(slotConsumer, anchorX, anchorY);\n    }\n\n    /**\n     * Move the top-left anchor point of generated grids to ({@code anchorX}, {@code anchorY})\n     */\n    public SlotGenerator moveTo(int anchorX, int anchorY) {\n        this.anchorX = anchorX;\n        this.anchorY = anchorY;\n        return this;\n    }\n\n    /**\n     * Shorthand for calling both {@link #horizontalSpacing} and\n     * {@link #verticalSpacing} with {@code spacing}\n     */\n    public SlotGenerator spacing(int spacing) {\n        this.horizontalSpacing = spacing;\n        this.verticalSpacing = spacing;\n        return this;\n    }\n\n    public SlotGenerator horizontalSpacing(int horizontalSpacing) {\n        this.horizontalSpacing = horizontalSpacing;\n        return this;\n    }\n\n    public SlotGenerator verticalSpacing(int verticalSpacing) {\n        this.verticalSpacing = verticalSpacing;\n        return this;\n    }\n\n    public SlotGenerator slotConsumer(Consumer<Slot> slotConsumer) {\n        this.slotConsumer = slotConsumer;\n        return this;\n    }\n\n    /**\n     * Reset the slot factory of this generator\n     * to the default {@link Slot#Slot(Container, int, int, int)} constructor\n     */\n    public SlotGenerator defaultSlotFactory() {\n        this.slotFactory = Slot::new;\n        return this;\n    }\n\n    /**\n     * Set the slot factory of this generator, used for instantiating\n     * each generated slot, to {@code slotFactory}\n     */\n    public SlotGenerator slotFactory(SlotFactory slotFactory) {\n        this.slotFactory = slotFactory;\n        return this;\n    }\n\n    public SlotGenerator grid(Container container, int startIndex, int width, int height) {\n        for (int row = 0; row < height; row++) {\n            for (int column = 0; column < width; column++) {\n                slotConsumer.accept(this.slotFactory.create(\n                        container,\n                        startIndex + row * width + column,\n                        anchorX + column * (18 + this.horizontalSpacing),\n                        anchorY + row * (18 + this.verticalSpacing)\n                ));\n            }\n        }\n\n        return this;\n    }\n\n    public SlotGenerator playerInventory(Inventory playerInventory) {\n        this.grid(playerInventory, 9, 9, 3);\n        this.anchorY += 58;\n        this.grid(playerInventory, 0, 9, 1);\n        this.anchorY -= 58;\n\n        return this;\n    }\n\n    @FunctionalInterface\n    public interface SlotFactory {\n        Slot create(Container container, int index, int x, int y);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/screens/SyncedProperty.java",
    "content": "package io.wispforest.owo.client.screens;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.owo.serialization.RegistriesAttribute;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport org.jetbrains.annotations.ApiStatus;\n\npublic class SyncedProperty<T> extends Observable<T> {\n    private final int index;\n    private final Endec<T> endec;\n    private final AbstractContainerMenu owner;\n    private boolean needsSync;\n\n    @ApiStatus.Internal\n    public SyncedProperty(int index, Endec<T> endec, T initial, AbstractContainerMenu owner) {\n        super(initial);\n\n        this.index = index;\n        this.endec = endec;\n        this.owner = owner;\n    }\n\n    public int index() {\n        return index;\n    }\n\n    @ApiStatus.Internal\n    public boolean needsSync() {\n        return needsSync;\n    }\n\n    @ApiStatus.Internal\n    public void write(FriendlyByteBuf buf) {\n        needsSync = false;\n        buf.write(serializationContext(), this.endec, value);\n    }\n\n    @ApiStatus.Internal\n    public void read(FriendlyByteBuf buf) {\n        this.set(buf.read(serializationContext(), this.endec));\n    }\n\n    @Override\n    protected void notifyObservers(T value) {\n        super.notifyObservers(value);\n\n        this.needsSync = true;\n    }\n\n    public void markDirty() {\n        notifyObservers(value);\n    }\n\n    private SerializationContext serializationContext() {\n        var player = this.owner.player();\n        if (player == null) return SerializationContext.empty();\n\n        return SerializationContext.attributes(RegistriesAttribute.of(player.registryAccess()));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/screens/ValidatingSlot.java",
    "content": "package io.wispforest.owo.client.screens;\n\nimport net.minecraft.world.Container;\nimport net.minecraft.world.inventory.Slot;\nimport net.minecraft.world.item.ItemStack;\n\nimport java.util.function.Predicate;\n\n/**\n * A slot that uses the provided {@code insertCondition}\n * to decide which items can be inserted\n */\npublic class ValidatingSlot extends Slot {\n\n    private final Predicate<ItemStack> insertCondition;\n\n    public ValidatingSlot(Container container, int index, int x, int y, Predicate<ItemStack> insertCondition) {\n        super(container, index, x, y);\n        this.insertCondition = insertCondition;\n    }\n\n    @Override\n    public boolean mayPlace(ItemStack stack) {\n        return insertCondition.test(stack);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/texture/AnimatedTextureDrawable.java",
    "content": "package io.wispforest.owo.client.texture;\n\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.Renderable;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.Util;\n\n/**\n * A drawable that can draw an animated texture, very similar to how\n * .mcmeta works on stitched textures in ticked atlases\n *\n * <p>Originally from Animawid, adapted for oωo</p>\n *\n * @author Tempora\n * @author glisco\n */\npublic class AnimatedTextureDrawable implements Renderable {\n\n    private final SpriteSheetMetadata metadata;\n    private final Identifier texture;\n\n    private final int validFrames;\n    private final int delay;\n    private final boolean loop;\n    private final int rows;\n    private long startTime = -1L;\n\n    private final int width, height;\n    private int x, y;\n\n    /**\n     * Creates a new animated texture widget using the width and height of the spritesheet as dimensions\n     *\n     * @see #AnimatedTextureDrawable(int, int, int, int, Identifier, SpriteSheetMetadata, int, boolean)\n     */\n    public AnimatedTextureDrawable(int x, int y, Identifier texture, SpriteSheetMetadata metadata, int delay, boolean loop) {\n        this(x, y, metadata.width(), metadata.height(), texture, metadata, delay, loop);\n    }\n\n    /**\n     * Creates a new animated texture widget that can be placed on your Screen or overlay etc.\n     *\n     * @param x        The x position of the widget.\n     * @param y        The y position of the widget.\n     * @param width    The width of the widget.\n     * @param height   The height of the widget.\n     * @param texture  The identifier of the texture, eg: {@code mymod:texture/animation_spritesheet.png}\n     * @param metadata Metadata on the spritesheet.\n     * @param delay    The delay, in milliseconds, between each frame.\n     */\n    public AnimatedTextureDrawable(int x, int y, int width, int height, Identifier texture, SpriteSheetMetadata metadata, int delay, boolean loop) {\n        this.x = x;\n        this.y = y;\n        this.texture = texture;\n        this.delay = delay;\n        this.metadata = metadata;\n        this.width = width;\n        this.height = height;\n        this.loop = loop;\n\n        int columns = metadata.width() / metadata.frameWidth();\n        this.rows = metadata.height() / metadata.frameHeight();\n        this.validFrames = columns * this.rows;\n    }\n\n    /**\n     * Renders this drawable at the given position. The position\n     * of this drawable is mutated non-temporarily\n     */\n    public void render(int x, int y, GuiGraphics context, int mouseX, int mouseY, float delta) {\n        this.x = x;\n        this.y = y;\n        this.render(context, mouseX, mouseY, delta);\n    }\n\n    @SuppressWarnings(\"IntegerDivisionInFloatingPointContext\")\n    @Override\n    public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {\n        if (startTime == -1L) startTime = Util.getMillis();\n\n        long currentTime = Util.getMillis();\n        long frame = Math.min(validFrames - 1, (currentTime - startTime) / delay);\n\n        if (loop && frame == validFrames - 1) {\n            startTime = Util.getMillis();\n            frame = 0;\n        }\n\n        context.blit(RenderPipelines.GUI_TEXTURED, this.texture, x, y, (frame / rows) * metadata.frameWidth(), (frame % rows) * metadata.frameHeight(), width, height, metadata.width(), metadata.height());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/client/texture/SpriteSheetMetadata.java",
    "content": "package io.wispforest.owo.client.texture;\n\n\n/**\n * A simple container to define the sprite sheet an {@link AnimatedTextureDrawable} uses\n *\n * <p>Originally from Animawid, adapted for oωo</p>\n *\n * @author Tempora\n * @author glisco\n */\npublic record SpriteSheetMetadata(int width, int height, int frameWidth, int frameHeight, int offset) {\n\n    /**\n     * Creates a new SpriteSheetMetadata object.\n     *\n     * @param width       The width of the Sprite Sheet.\n     * @param height      The height of the Sprite Sheet.\n     * @param frameWidth  The width of each individual frame\n     * @param frameHeight The width of each individual frame\n     */\n    public SpriteSheetMetadata(int width, int height, int frameWidth, int frameHeight) {\n        this(width, height, frameWidth, frameHeight, 0);\n    }\n\n    /**\n     * Convenience constructor that assumes both the spritesheet and frames are square\n     */\n    public SpriteSheetMetadata(int size, int frameSize) {\n        this(size, size, frameSize, frameSize, 0);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/command/EnumArgumentType.java",
    "content": "package io.wispforest.owo.command;\n\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.arguments.ArgumentType;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport com.mojang.brigadier.exceptions.DynamicCommandExceptionType;\nimport com.mojang.brigadier.suggestion.Suggestions;\nimport com.mojang.brigadier.suggestion.SuggestionsBuilder;\nimport io.wispforest.owo.Owo;\nimport net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry;\nimport net.minecraft.commands.SharedSuggestionProvider;\nimport net.minecraft.commands.synchronization.SingletonArgumentInfo;\nimport net.minecraft.network.chat.Component;\n\nimport java.util.Arrays;\nimport java.util.Locale;\nimport java.util.concurrent.CompletableFuture;\n\n/**\n * A simple implementation of {@link ArgumentType} that works with any {@code enum}.\n * It is recommended to create one instance of this and use it both in the call\n * to {@link net.minecraft.commands.Commands#argument(String, ArgumentType)}\n * as well as for getting the supplied argument via {@link #get(CommandContext, String)}\n *\n * @param <T> The {@code enum} this instance can parse\n */\npublic class EnumArgumentType<T extends Enum<T>> implements ArgumentType<Enum<T>> {\n\n    private final DynamicCommandExceptionType noValueException;\n    private final String noElementMessage;\n    private final Class<T> enumClass;\n\n    private EnumArgumentType(Class<T> enumClass, String noElementMessage) {\n        this.enumClass = enumClass;\n        this.noElementMessage = noElementMessage;\n        this.noValueException = new DynamicCommandExceptionType(o -> Component.literal(this.noElementMessage.replace(\"{}\", o.toString())));\n    }\n\n    /**\n     * Creates a new instance that uses {@code Invalid enum value '{}'} as the\n     * error message if an invalid value is supplied. This <b>must</b> be called\n     * on both <b>server and client</b> so the serializer can be registered correctly.\n     * Since the instance is added to the type registry, this must happen during mod\n     * initialization when the registries are mutable\n     *\n     * @param enumClass The {@code enum} type to parse for\n     * @param <T>       The {@code enum} type to parse for\n     * @return A new argument type that can parse instances of {@code T}\n     */\n    public static <T extends Enum<T>> EnumArgumentType<T> create(Class<T> enumClass) {\n        final var type = new EnumArgumentType<>(enumClass, \"Invalid enum value '{}'\");\n        ArgumentTypeRegistry.registerArgumentType(Owo.id(\"enum_\" + enumClass.getName().toLowerCase(Locale.ROOT)), type.getClass(), SingletonArgumentInfo.contextFree(() -> type));\n        return type;\n    }\n\n    /**\n     * Creates a new instance that uses {@code noElementMessage} as the\n     * error message if an invalid value is supplied. This <b>must</b> be called\n     * on both <b>server and client</b> so the serializer can be registered correctly\n     * Since the instance is added to the type registry, this must happen during mod\n     * initialization when the registries are mutable\n     *\n     * @param enumClass        The {@code enum} type to parse for\n     * @param noElementMessage The error message to send if an invalid value is\n     *                         supplied, with an optional {@code {}} placeholder\n     *                         for the supplied value\n     * @param <T>              The {@code enum} type to parse for\n     * @return A new argument type that can parse instances of {@code T}\n     */\n    public static <T extends Enum<T>> EnumArgumentType<T> create(Class<T> enumClass, String noElementMessage) {\n        final var type = new EnumArgumentType<>(enumClass, noElementMessage);\n        ArgumentTypeRegistry.registerArgumentType(Owo.id(\"enum_\" + enumClass.getName().toLowerCase(Locale.ROOT)), type.getClass(), SingletonArgumentInfo.contextFree(() -> type));\n        return type;\n    }\n\n    public T get(CommandContext<?> context, String name) {\n        return context.getArgument(name, enumClass);\n    }\n\n    @Override\n    public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {\n        return SharedSuggestionProvider.suggest(Arrays.stream(enumClass.getEnumConstants()).map(Enum::toString), builder);\n    }\n\n    @Override\n    public T parse(StringReader reader) throws CommandSyntaxException {\n        final var name = reader.readString();\n        try {\n            return Enum.valueOf(enumClass, name);\n        } catch (IllegalArgumentException e) {\n            throw noValueException.create(name);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/command/debug/CcaDataCommand.java",
    "content": "package io.wispforest.owo.command.debug;\n\nimport com.mojang.brigadier.CommandDispatcher;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ops.TextOps;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.arguments.NbtPathArgument;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.NbtUtils;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.world.level.storage.TagValueOutput;\n\nimport static net.minecraft.commands.Commands.argument;\nimport static net.minecraft.commands.Commands.literal;\n\npublic class CcaDataCommand {\n\n    public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {\n        dispatcher.register(literal(\"cca-data\").executes(CcaDataCommand::executeDumpAll)\n                .then(argument(\"path\", NbtPathArgument.nbtPath()).executes(CcaDataCommand::executeDumpPath)));\n    }\n\n    private static int executeDumpAll(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        final var player = context.getSource().getPlayer();\n        final var writeView = TagValueOutput.createWithoutContext(new ProblemReporter.ScopedCollector(Owo.LOGGER));\n        player.save(writeView);\n\n        final var nbt = writeView.buildResult().getCompound(\"cardinal_components\").orElseGet(CompoundTag::new);\n\n        context.getSource().sendSuccess(() -> TextOps.concat(Owo.PREFIX, TextOps.withFormatting(\"CCA Data:\", ChatFormatting.GRAY)), false);\n        context.getSource().sendSuccess(() -> NbtUtils.toPrettyComponent(nbt), false);\n\n        return 0;\n    }\n\n    private static int executeDumpPath(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        final var player = context.getSource().getPlayer();\n        final var path = NbtPathArgument.getPath(context, \"path\");\n\n        final var writeView = TagValueOutput.createWithoutContext(new ProblemReporter.ScopedCollector(Owo.LOGGER));\n        player.save(writeView);\n\n        final var nbt = path.get(writeView.buildResult().getCompound(\"cardinal_components\").orElseGet(CompoundTag::new)).iterator().next();\n\n        context.getSource().sendSuccess(() -> TextOps.concat(Owo.PREFIX, TextOps.withFormatting(\"CCA Data:\", ChatFormatting.GRAY)), false);\n        context.getSource().sendSuccess(() -> NbtUtils.toPrettyComponent(nbt), false);\n\n        return 0;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/command/debug/DumpdataCommand.java",
    "content": "package io.wispforest.owo.command.debug;\n\nimport com.mojang.brigadier.Command;\nimport com.mojang.brigadier.CommandDispatcher;\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ops.TextOps;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.arguments.NbtPathArgument;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.component.DataComponentPatch;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.nbt.NbtOps;\nimport net.minecraft.nbt.NbtUtils;\nimport net.minecraft.nbt.Tag;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.world.entity.projectile.ProjectileUtil;\nimport net.minecraft.world.level.storage.TagValueOutput;\nimport net.minecraft.world.phys.BlockHitResult;\nimport net.minecraft.world.phys.HitResult;\n\nimport java.util.regex.Pattern;\n\nimport static net.minecraft.commands.Commands.argument;\nimport static net.minecraft.commands.Commands.literal;\n\npublic class DumpdataCommand {\n\n    private static final int GENERAL_PURPLE = 0xB983FF;\n    private static final int KEY_BLUE = 0x94B3FD;\n    private static final int VALUE_BLUE = 0x94DAFF;\n\n    public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {\n        dispatcher.register(literal(\"dumpdata\")\n                .then(literal(\"item\").executes(withRootPath(DumpdataCommand::executeItem))\n                        .then(argument(\"nbt_path\", NbtPathArgument.nbtPath()).executes(withPathArg(DumpdataCommand::executeItem))))\n                .then(literal(\"block\").executes(withRootPath(DumpdataCommand::executeBlock))\n                        .then(argument(\"nbt_path\", NbtPathArgument.nbtPath()).executes(withPathArg(DumpdataCommand::executeBlock))))\n                .then(literal(\"entity\").executes(withRootPath(DumpdataCommand::executeEntity))\n                        .then(argument(\"nbt_path\", NbtPathArgument.nbtPath()).executes(withPathArg(DumpdataCommand::executeEntity)))));\n    }\n\n    private static Command<CommandSourceStack> withRootPath(DataDumper dumper) {\n        return context -> dumper.dump(context, NbtPathArgument.nbtPath().parse(new StringReader(\"\")));\n    }\n\n    private static Command<CommandSourceStack> withPathArg(DataDumper dumper) {\n        return context -> {\n            final var path = NbtPathArgument.getPath(context, \"nbt_path\");\n            return dumper.dump(context, path);\n        };\n    }\n\n    private static int executeItem(CommandContext<CommandSourceStack> context, NbtPathArgument.NbtPath path) throws CommandSyntaxException {\n        final var source = context.getSource();\n        final var stack = source.getPlayer().getMainHandItem();\n\n        informationHeader(source, \"Item\");\n        sendIdentifier(source, stack.getItem(), BuiltInRegistries.ITEM);\n\n        if (stack.get(DataComponents.MAX_DAMAGE) != null) {\n            feedback(source, TextOps.withColor(\"Durability: §\" + stack.get(DataComponents.MAX_DAMAGE),\n                    TextOps.color(ChatFormatting.GRAY), KEY_BLUE));\n        } else {\n            feedback(source, TextOps.withFormatting(\"Not damageable\", ChatFormatting.GRAY));\n        }\n\n        if (!stack.getComponentsPatch().isEmpty()) {\n            feedback(source, TextOps.withFormatting(\"Component changes\" + formatPath(path) + \": \", ChatFormatting.GRAY)\n                    .append(NbtUtils.toPrettyComponent(getPath(DataComponentPatch.CODEC.encodeStart(NbtOps.INSTANCE, stack.getComponentsPatch()).getOrThrow(), path))));\n        } else {\n            feedback(source, TextOps.withFormatting(\"No component changes\", ChatFormatting.GRAY));\n        }\n\n        feedback(source, TextOps.withFormatting(\"-----------------------\", ChatFormatting.GRAY));\n\n        return 0;\n    }\n\n    private static int executeEntity(CommandContext<CommandSourceStack> context, NbtPathArgument.NbtPath path) throws CommandSyntaxException {\n        final var source = context.getSource();\n        final var player = source.getPlayer();\n\n        final var target = ProjectileUtil.getEntityHitResult(\n                player,\n                player.getEyePosition(0),\n                player.getEyePosition(0).add(player.getViewVector(0).scale(5)),\n                player.getBoundingBox().expandTowards(player.getViewVector(0).scale(5)).inflate(1),\n                entity -> true,\n                5 * 5);\n\n        if (target == null || target.getType() != HitResult.Type.ENTITY) {\n            source.sendFailure(TextOps.concat(Owo.PREFIX, Component.literal(\"You're not looking at an entity\")));\n            return 1;\n        }\n\n        final var entity = target.getEntity();\n\n        informationHeader(source, \"Entity\");\n        sendIdentifier(source, entity.getType(), BuiltInRegistries.ENTITY_TYPE);\n\n        var writeView = TagValueOutput.createWithoutContext(new ProblemReporter.ScopedCollector(Owo.LOGGER));\n        entity.save(writeView);\n\n        feedback(source, TextOps.withFormatting(\"NBT\" + formatPath(path) + \": \", ChatFormatting.GRAY)\n                .append(NbtUtils.toPrettyComponent(getPath(writeView.buildResult(), path))));\n\n        feedback(source, TextOps.withFormatting(\"-----------------------\", ChatFormatting.GRAY));\n\n        return 0;\n    }\n\n    private static int executeBlock(CommandContext<CommandSourceStack> context, NbtPathArgument.NbtPath path) throws CommandSyntaxException {\n        final var source = context.getSource();\n        final var player = source.getPlayer();\n\n        final var target = player.pick(5, 0, false);\n\n        if (target.getType() != HitResult.Type.BLOCK) {\n            source.sendFailure(TextOps.concat(Owo.PREFIX, Component.literal(\"You're not looking at a block\")));\n            return 1;\n        }\n\n        final var pos = ((BlockHitResult) target).getBlockPos();\n\n        final var blockState = player.level().getBlockState(pos);\n        final var blockStateString = blockState.toString();\n\n        informationHeader(source, \"Block\");\n        sendIdentifier(source, blockState.getBlock(), BuiltInRegistries.BLOCK);\n\n        if (blockStateString.contains(\"[\")) {\n            feedback(source, TextOps.withFormatting(\"State properties: \", ChatFormatting.GRAY));\n\n            var stateString = blockStateString.split(Pattern.quote(\"[\"))[1];\n            stateString = stateString.substring(0, stateString.length() - 1);\n            var stateInfo = stateString.replaceAll(\"=\", \": §\").split(\",\");\n\n            for (var property : stateInfo) {\n                feedback(source, TextOps.withColor(\"    \" + property, KEY_BLUE, VALUE_BLUE));\n            }\n        } else {\n            feedback(source, TextOps.withFormatting(\"No state properties\", ChatFormatting.GRAY));\n        }\n\n        final var blockEntity = player.level().getBlockEntity(pos);\n        if (blockEntity != null) {\n            feedback(source, TextOps.withFormatting(\"Block Entity NBT\" + formatPath(path) + \": \", ChatFormatting.GRAY)\n                    .append(NbtUtils.toPrettyComponent(getPath(blockEntity.saveWithoutMetadata(player.registryAccess()), path))));\n        } else {\n            feedback(source, TextOps.withFormatting(\"No block entity\", ChatFormatting.GRAY));\n        }\n\n        feedback(source, TextOps.withFormatting(\"-----------------------\", ChatFormatting.GRAY));\n\n        return 0;\n    }\n\n    private static <T> void sendIdentifier(CommandSourceStack source, T object, Registry<T> registry) {\n        final var id = registry.getKey(object).toString().split(\":\");\n        feedback(source, TextOps.withColor(\"Identifier: §\" + id[0] + \":§\" + id[1], TextOps.color(ChatFormatting.GRAY), KEY_BLUE, VALUE_BLUE));\n    }\n\n    private static void informationHeader(CommandSourceStack source, String name) {\n        feedback(source, TextOps.withColor(\"---[§ \" + name + \" Information §]---\",\n                TextOps.color(ChatFormatting.GRAY), GENERAL_PURPLE, TextOps.color(ChatFormatting.GRAY)));\n    }\n\n    private static void feedback(CommandSourceStack source, Component message) {\n        source.sendSuccess(() -> message, false);\n    }\n\n    private static String formatPath(NbtPathArgument.NbtPath path) {\n        return path.toString().isBlank() ? \"\" : \"(\" + path + \")\";\n    }\n\n    private static Tag getPath(Tag nbt, NbtPathArgument.NbtPath path) throws CommandSyntaxException {\n        return path.get(nbt).iterator().next();\n    }\n\n    @FunctionalInterface\n    private interface DataDumper {\n        int dump(CommandContext<CommandSourceStack> context, NbtPathArgument.NbtPath path) throws CommandSyntaxException;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/command/debug/HealCommand.java",
    "content": "package io.wispforest.owo.command.debug;\n\nimport com.mojang.brigadier.CommandDispatcher;\nimport com.mojang.brigadier.arguments.FloatArgumentType;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ops.TextOps;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.arguments.EntityArgument;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.LivingEntity;\n\nimport static net.minecraft.commands.Commands.argument;\nimport static net.minecraft.commands.Commands.literal;\n\npublic class HealCommand {\n\n    public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {\n        dispatcher.register(literal(\"heal\")\n                .executes(HealCommand::executeFullHeal)\n                .then(argument(\"amount\", FloatArgumentType.floatArg(0))\n                        .executes(HealCommand::executeSelfHeal))\n                .then(argument(\"entity\", EntityArgument.entity())\n                        .executes(HealCommand::executeTargetedFullHeal)\n                        .then(argument(\"amount\", FloatArgumentType.floatArg(0))\n                                .executes(HealCommand::executeTargetedHeal))));\n    }\n\n    private static int executeFullHeal(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        var target = context.getSource().getEntityOrException();\n        return executeHeal(\n                context,\n                target,\n                target instanceof LivingEntity living ? living.getMaxHealth() : Float.MAX_VALUE\n        );\n    }\n\n    private static int executeSelfHeal(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        return executeHeal(\n                context,\n                context.getSource().getEntityOrException(),\n                FloatArgumentType.getFloat(context, \"amount\")\n        );\n    }\n\n    private static int executeTargetedFullHeal(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        var target = EntityArgument.getEntity(context, \"entity\");\n        return executeHeal(\n                context,\n                target,\n                target instanceof LivingEntity living ? living.getMaxHealth() : Float.MAX_VALUE\n        );\n    }\n\n    private static int executeTargetedHeal(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n        return executeHeal(\n                context,\n                EntityArgument.getEntity(context, \"entity\"),\n                FloatArgumentType.getFloat(context, \"amount\")\n        );\n    }\n\n    private static int executeHeal(CommandContext<CommandSourceStack> context, Entity entity, float amount) throws CommandSyntaxException {\n        if (entity instanceof LivingEntity living) {\n            float healed = living.getHealth();\n            living.heal(amount);\n            healed = living.getHealth() - healed;\n\n            float thankYouMojang = healed;\n            context.getSource().sendSuccess(() -> TextOps.concat(Owo.PREFIX, TextOps.withColor(\"healed §\" + thankYouMojang + \" §hp\",\n                    TextOps.color(ChatFormatting.GRAY), OwoDebugCommands.GENERAL_PURPLE, TextOps.color(ChatFormatting.GRAY))), false);\n        } else {\n            context.getSource().sendFailure(TextOps.concat(Owo.PREFIX, Component.nullToEmpty(\"Cannot heal non living entity\")));\n        }\n\n        return (int) Math.floor(amount);\n    }\n\n}\n\n//chyz was here"
  },
  {
    "path": "src/main/java/io/wispforest/owo/command/debug/MakeLootContainerCommand.java",
    "content": "package io.wispforest.owo.command.debug;\n\nimport com.mojang.brigadier.CommandDispatcher;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.arguments.ResourceOrIdArgument;\nimport net.minecraft.commands.arguments.item.ItemArgument;\n\nimport static net.minecraft.commands.Commands.argument;\nimport static net.minecraft.commands.Commands.literal;\n\npublic class MakeLootContainerCommand {\n\n    public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext registryAccess) {\n        dispatcher.register(literal(\"make-loot-container\")\n                .then(argument(\"item\", ItemArgument.item(registryAccess))\n                        .then(argument(\"loot_table\", ResourceOrIdArgument.lootTable(registryAccess))\n                                .executes(MakeLootContainerCommand::execute))));\n    }\n\n    // TODO: reimplement\n    private static int execute(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {\n//        var targetStack = ItemStackArgumentType.getItemStackArgument(context, \"item\").createStack(1, false);\n//        var tableId = RegistryEntryArgumentType.getLootTable(context, \"loot_table\");\n//\n//        var blockEntityTag = targetStack.get(DataComponentTypes.BLOCK_ENTITY_DATA);\n//        if (blockEntityTag == null) {\n//            blockEntityTag = TypedEntityData.create()\n//        }\n//\n//        blockEntityTag = blockEntityTag.apply(x -> {\n//            x.putString(\"LootTable\", tableId.getIdAsString());\n//        });\n//        targetStack.set(DataComponentTypes.BLOCK_ENTITY_DATA, blockEntityTag);\n//\n//        context.getSource().getPlayer().getInventory().offerOrDrop(targetStack);\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/command/debug/OwoDebugCommands.java",
    "content": "package io.wispforest.owo.command.debug;\n\nimport com.mojang.brigadier.arguments.IntegerArgumentType;\nimport com.mojang.brigadier.arguments.StringArgumentType;\nimport com.mojang.brigadier.exceptions.SimpleCommandExceptionType;\nimport com.mojang.brigadier.suggestion.SuggestionProvider;\nimport com.mojang.logging.LogUtils;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.command.EnumArgumentType;\nimport io.wispforest.owo.ops.TextOps;\nimport io.wispforest.owo.renderdoc.RenderDoc;\nimport io.wispforest.owo.renderdoc.RenderdocScreen;\nimport io.wispforest.owo.ui.hud.HudInspectorScreen;\nimport io.wispforest.owo.ui.parsing.ConfigureHotReloadScreen;\nimport io.wispforest.owo.ui.parsing.UIModelLoader;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;\nimport net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;\nimport net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;\nimport net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.commands.SharedSuggestionProvider;\nimport net.minecraft.commands.arguments.IdentifierArgument;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.network.chat.ClickEvent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.HoverEvent;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.world.entity.ai.village.poi.PoiManager;\nimport net.minecraft.world.phys.BlockHitResult;\nimport net.minecraft.world.phys.HitResult;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.slf4j.event.Level;\n\nimport static net.minecraft.commands.Commands.argument;\nimport static net.minecraft.commands.Commands.literal;\n\n@ApiStatus.Internal\npublic class OwoDebugCommands {\n\n    private static final EnumArgumentType<Level> LEVEL_ARGUMENT_TYPE =\n        EnumArgumentType.create(Level.class, \"'{}' is not a valid logging level\");\n\n    private static final SuggestionProvider<CommandSourceStack> POI_TYPES =\n        (context, builder) -> SharedSuggestionProvider.suggestResource(BuiltInRegistries.POINT_OF_INTEREST_TYPE.keySet(), builder);\n\n    private static final SimpleCommandExceptionType NO_POI_TYPE = new SimpleCommandExceptionType(Component.nullToEmpty(\"Invalid POI type\"));\n    public static final int GENERAL_PURPLE = 0xB983FF;\n    public static final int KEY_BLUE = 0x94B3FD;\n    public static final int VALUE_BLUE = 0x94DAFF;\n\n    public static void register() {\n        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {\n\n            dispatcher.register(literal(\"logger\").then(argument(\"level\", LEVEL_ARGUMENT_TYPE).executes(context -> {\n                final var level = LEVEL_ARGUMENT_TYPE.get(context, \"level\");\n                LogUtils.configureRootLoggingLevel(level);\n\n                context.getSource().sendSuccess(() -> TextOps.concat(Owo.PREFIX, Component.nullToEmpty(\"global logging level set to: §9\" + level)), false);\n                return 0;\n            })));\n\n            dispatcher.register(literal(\"query-poi\").then(argument(\"poi_type\", IdentifierArgument.id()).suggests(POI_TYPES)\n                .then(argument(\"radius\", IntegerArgumentType.integer()).executes(context -> {\n                    var player = context.getSource().getPlayer();\n                    var poiType = BuiltInRegistries.POINT_OF_INTEREST_TYPE.getOptional(IdentifierArgument.getId(context, \"poi_type\"))\n                        .orElseThrow(NO_POI_TYPE::create);\n\n                    var entries = ((ServerLevel) player.level()).getPoiManager().getInRange(type -> type.value() == poiType,\n                        player.blockPosition(), IntegerArgumentType.getInteger(context, \"radius\"), PoiManager.Occupancy.ANY).toList();\n\n                    player.displayClientMessage(TextOps.concat(Owo.PREFIX, TextOps.withColor(\"Found §\" + entries.size() + \" §entr\" + (entries.size() == 1 ? \"y\" : \"ies\"),\n                        TextOps.color(ChatFormatting.GRAY), GENERAL_PURPLE, TextOps.color(ChatFormatting.GRAY))), false);\n\n                    for (var entry : entries) {\n\n                        final var entryPos = entry.getPos();\n                        final var blockId = BuiltInRegistries.BLOCK.getKey(player.level().getBlockState(entryPos).getBlock()).toString();\n                        final var posString = \"(\" + entryPos.getX() + \" \" + entryPos.getY() + \" \" + entryPos.getZ() + \")\";\n\n                        final var message = TextOps.withColor(\"-> §\" + blockId + \" §\" + posString,\n                            TextOps.color(ChatFormatting.GRAY), KEY_BLUE, VALUE_BLUE);\n\n                        message.withStyle(style -> style.withClickEvent(new ClickEvent.SuggestCommand(\n                                \"/tp \" + entryPos.getX() + \" \" + entryPos.getY() + \" \" + entryPos.getZ()))\n                            .withHoverEvent(new HoverEvent.ShowText(Component.nullToEmpty(\"Click to teleport\"))));\n\n                        player.displayClientMessage(message, false);\n                    }\n\n                    return entries.size();\n                }))));\n\n            dispatcher.register(literal(\"dumpfield\").then(argument(\"field_name\", StringArgumentType.string()).executes(context -> {\n                final var targetField = StringArgumentType.getString(context, \"field_name\");\n                final CommandSourceStack source = context.getSource();\n                final ServerPlayer player = source.getPlayer();\n                HitResult target = player.pick(5, 0, false);\n\n                if (target.getType() != HitResult.Type.BLOCK) {\n                    source.sendFailure(TextOps.concat(Owo.PREFIX, Component.literal(\"You're not looking at a block\")));\n                    return 1;\n                }\n\n                BlockPos pos = ((BlockHitResult) target).getBlockPos();\n                final var blockEntity = player.level().getBlockEntity(pos);\n\n                if (blockEntity == null) {\n                    source.sendFailure(TextOps.concat(Owo.PREFIX, Component.literal((\"No block entity\"))));\n                    return 1;\n                }\n\n                var blockEntityClass = blockEntity.getClass();\n\n                try {\n                    final var field = blockEntityClass.getDeclaredField(targetField);\n\n                    if (!field.canAccess(blockEntity)) field.setAccessible(true);\n                    final var value = field.get(blockEntity);\n\n                    source.sendSuccess(() -> TextOps.concat(Owo.PREFIX, TextOps.withColor(\"Field value: §\" + value, TextOps.color(ChatFormatting.GRAY), KEY_BLUE)), false);\n\n                } catch (Exception e) {\n                    source.sendFailure(TextOps.concat(Owo.PREFIX, Component.literal(\"Could not access field - \" + e.getClass().getSimpleName() + \": \" + e.getMessage())));\n                }\n\n                return 0;\n            })));\n\n            MakeLootContainerCommand.register(dispatcher, registryAccess);\n            DumpdataCommand.register(dispatcher);\n            HealCommand.register(dispatcher);\n\n            if (FabricLoader.getInstance().isModLoaded(\"cardinal-components-base\")) {\n                CcaDataCommand.register(dispatcher);\n            }\n        });\n    }\n\n    @Environment(EnvType.CLIENT)\n    public static class Client {\n\n        private static final SuggestionProvider<FabricClientCommandSource> LOADED_UI_MODELS =\n            (context, builder) -> SharedSuggestionProvider.suggestResource(UIModelLoader.allLoadedModels(), builder);\n\n        private static final SimpleCommandExceptionType NO_SUCH_UI_MODEL = new SimpleCommandExceptionType(Component.literal(\"No such UI model is loaded\"));\n\n        public static void register() {\n            ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {\n                dispatcher.register(ClientCommandManager.literal(\"owo-hud-inspect\")\n                    .executes(context -> {\n                        Minecraft.getInstance().setScreen(new HudInspectorScreen());\n                        return 0;\n                    }));\n\n                dispatcher.register(ClientCommandManager.literal(\"owo-ui-set-reload-path\")\n                    .then(ClientCommandManager.argument(\"model-id\", IdentifierArgument.id()).suggests(LOADED_UI_MODELS).executes(context -> {\n                        var modelId = context.getArgument(\"model-id\", Identifier.class);\n                        if (UIModelLoader.getPreloaded(modelId) == null) throw NO_SUCH_UI_MODEL.create();\n\n                        Minecraft.getInstance().setScreen(new ConfigureHotReloadScreen(modelId, null));\n                        return 0;\n                    })));\n\n                if (RenderDoc.isAvailable()) {\n                    dispatcher.register(ClientCommandManager.literal(\"renderdoc\").executes(context -> {\n                        Minecraft.getInstance().setScreen(new RenderdocScreen());\n                        return 1;\n                    }).then(ClientCommandManager.literal(\"comment\")\n                        .then(ClientCommandManager.argument(\"capture_index\", IntegerArgumentType.integer(0))\n                            .then(ClientCommandManager.argument(\"comment\", StringArgumentType.greedyString())\n                                .executes(context -> {\n                                    var capture = RenderDoc.getCapture(IntegerArgumentType.getInteger(context, \"capture_index\"));\n                                    if (capture == null) {\n                                        context.getSource().sendError(TextOps.concat(Owo.PREFIX, Component.nullToEmpty(\"no such capture\")));\n                                        return 0;\n                                    }\n\n                                    RenderDoc.setCaptureComments(capture, StringArgumentType.getString(context, \"comment\"));\n                                    context.getSource().sendFeedback(TextOps.concat(Owo.PREFIX, Component.nullToEmpty(\"comment updated\")));\n\n                                    return 1;\n                                })))));\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/compat/emi/EmiStackUtil.java",
    "content": "package io.wispforest.owo.compat.emi;\n\nimport dev.emi.emi.api.FabricEmiStack;\nimport dev.emi.emi.api.stack.EmiStack;\nimport io.wispforest.owo.util.ViewerStack;\nimport net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.material.Fluid;\n\npublic class EmiStackUtil {\n    public static ViewerStack fromEmi(EmiStack stack) {\n        if (stack.getKey() instanceof Item item) {\n            return ViewerStack.OfItem.of(stack.getItemStack());\n        } else if (stack.getKey() instanceof Fluid fluid) {\n            return new ViewerStack.OfFluid(FluidVariant.of(fluid, stack.getComponentChanges()), stack.getAmount());\n        } else {\n            // TODO: custom EMI stack.\n            return ViewerStack.OfItem.EMPTY;\n        }\n    }\n\n    public static EmiStack toEmi(ViewerStack stack) {\n        if (stack instanceof ViewerStack.OfItem ofItem) {\n            return EmiStack.of(ofItem.asStack());\n        } else if (stack instanceof ViewerStack.OfFluid ofFluid) {\n            return FabricEmiStack.of(ofFluid.fluid(), ofFluid.count());\n        } else {\n            throw new IllegalStateException(\"Invalid ViewerStack\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/compat/emi/OwoEmiPlugin.java",
    "content": "package io.wispforest.owo.compat.emi;\n\nimport dev.emi.emi.api.EmiDragDropHandler;\nimport dev.emi.emi.api.EmiPlugin;\nimport dev.emi.emi.api.EmiRegistry;\nimport dev.emi.emi.api.stack.EmiIngredient;\nimport dev.emi.emi.api.stack.EmiStackInteraction;\nimport dev.emi.emi.api.widget.Bounds;\nimport io.wispforest.owo.braid.core.BraidScreen;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.widgets.recipeviewer.RecipeViewerExclusionZone;\nimport io.wispforest.owo.braid.widgets.recipeviewer.RecipeViewerStack;\nimport io.wispforest.owo.braid.widgets.recipeviewer.StackDropArea;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.mixin.itemgroup.CreativeModeInventoryScreenAccessor;\nimport io.wispforest.owo.ui.base.BaseOwoContainerScreen;\nimport io.wispforest.owo.util.pond.OwoCreativeInventoryScreenExtensions;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;\nimport net.minecraft.world.phys.AABB;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class OwoEmiPlugin implements EmiPlugin {\n    @Override\n    public void register(EmiRegistry registry) {\n        registry.addExclusionArea(CreativeModeInventoryScreen.class, (screen, consumer) -> {\n            var group = CreativeModeInventoryScreenAccessor.owo$getSelectedTab();\n            if (!(group instanceof OwoItemGroup owoGroup)) return;\n            if (owoGroup.getButtons().isEmpty()) return;\n\n            int x = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootX();\n            int y = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootY();\n\n            int stackHeight = owoGroup.getButtonStackHeight();\n            y -= 13 * (stackHeight - 4);\n\n            for (int i = 0; i < owoGroup.getButtons().size(); i++) {\n                int xOffset = x + 198 + (i / stackHeight) * 26;\n                int yOffset = y + 10 + (i % stackHeight) * 30;\n                consumer.accept(new Bounds(xOffset, yOffset, 24, 24));\n            }\n        });\n\n        registry.addGenericExclusionArea((screen, consumer) -> {\n            if (!(screen instanceof BaseOwoContainerScreen<?, ?> owoHandledScreen)) return;\n\n            owoHandledScreen.componentsForExclusionAreas()\n                .map(component -> new Bounds(component.x(), component.y(), component.width(), component.height()))\n                .forEach(consumer);\n        });\n\n        registry.addGenericExclusionArea((screen, consumer) -> {\n            if (!(screen instanceof BraidScreen braid)) return;\n\n            var visitor = new WidgetInstance.Visitor() {\n                @Override\n                public void visit(WidgetInstance<?> child) {\n                    if (child instanceof RecipeViewerExclusionZone.Instance area) {\n                        var bounds = area.computeGlobalBounds();\n\n                        consumer.accept(new Bounds((int) bounds.minX, (int) bounds.minY, (int) (bounds.maxX - bounds.minX), (int) (bounds.maxY - bounds.minY)));\n                    }\n\n                    child.visitChildren(this);\n                }\n            };\n\n            braid.state.rootInstance().visitChildren(visitor);\n        });\n\n        registry.addGenericStackProvider((screen, x, y) -> {\n            if (!(screen instanceof BraidScreen braid)) return EmiStackInteraction.EMPTY;\n\n            var hit = braid.state.hitTest(x, y)\n                .firstWhere(i -> i.instance() instanceof RecipeViewerStack.Instance);\n\n            if (hit == null) return EmiStackInteraction.EMPTY;\n\n            var instance = (RecipeViewerStack.Instance) hit.instance();\n\n            return new EmiStackInteraction(EmiStackUtil.toEmi(instance.widget().stackProvider.get()));\n        });\n\n        registry.addGenericDragDropHandler(new EmiDragDropHandler<>() {\n            @Override\n            public boolean dropStack(Screen screen, EmiIngredient stack, int x, int y) {\n                if (!(screen instanceof BraidScreen braid)) return false;\n\n                var hit = braid.state.hitTest(x, y)\n                    .firstWhere(i -> i.instance() instanceof StackDropArea.Instance);\n\n                if (hit == null) return false;\n\n                var instance = (StackDropArea.Instance) hit.instance();\n\n                var converted = EmiStackUtil.fromEmi(stack.getEmiStacks().get(0));\n\n                if (!instance.widget().stackPredicate.test(converted)) return false;\n\n                instance.widget().stackAcceptor.accept(converted);\n\n                return true;\n            }\n\n            @Override\n            public void render(Screen screen, EmiIngredient dragged, GuiGraphics draw, int mouseX, int mouseY, float delta) {\n                if (!(screen instanceof BraidScreen braid)) return;\n\n                List<AABB> allBounds = new ArrayList<>();\n\n                var converted = EmiStackUtil.fromEmi(dragged.getEmiStacks().get(0));\n\n                var visitor = new WidgetInstance.Visitor() {\n                    @Override\n                    public void visit(WidgetInstance<?> child) {\n                        if (child instanceof StackDropArea.Instance area && area.widget().stackPredicate.test(converted)) {\n                            allBounds.add(area.computeGlobalBounds());\n                        }\n\n                        child.visitChildren(this);\n                    }\n                };\n\n                braid.state.rootInstance().visitChildren(visitor);\n\n                for (AABB b : allBounds) {\n                    draw.fill((int) b.minX, (int) b.minY, (int) b.maxX, (int) b.maxY, 0x8822BB33);\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/compat/modmenu/OwoModMenuPlugin.java",
    "content": "package io.wispforest.owo.compat.modmenu;\n\nimport com.google.common.collect.ForwardingMap;\nimport com.terraformersmc.modmenu.api.ConfigScreenFactory;\nimport com.terraformersmc.modmenu.api.ModMenuApi;\nimport io.wispforest.owo.config.ui.ConfigScreenProviders;\nimport net.minecraft.util.Util;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@ApiStatus.Internal\npublic class OwoModMenuPlugin implements ModMenuApi {\n\n    private static final Map<String, ConfigScreenFactory<?>> OWO_FACTORIES = new ForwardingMap<>() {\n        @Override\n        protected @NotNull Map<String, ConfigScreenFactory<?>> delegate() {\n            return Util.make(\n                    new HashMap<>(),\n                    map -> ConfigScreenProviders.forEach((s, provider) -> map.put(s, provider::apply))\n            );\n        }\n    };\n\n    @Override\n    public Map<String, ConfigScreenFactory<?>> getProvidedConfigScreenFactories() {\n        return OWO_FACTORIES;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/compat/rei/OwoReiPlugin.java",
    "content": "package io.wispforest.owo.compat.rei;\n\nimport dev.architectury.event.CompoundEventResult;\nimport io.wispforest.owo.braid.core.BraidScreen;\nimport io.wispforest.owo.braid.framework.instance.WidgetInstance;\nimport io.wispforest.owo.braid.widgets.recipeviewer.RecipeViewerExclusionZone;\nimport io.wispforest.owo.braid.widgets.recipeviewer.RecipeViewerStack;\nimport io.wispforest.owo.braid.widgets.recipeviewer.StackDropArea;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.mixin.itemgroup.CreativeModeInventoryScreenAccessor;\nimport io.wispforest.owo.ui.base.BaseOwoContainerScreen;\nimport io.wispforest.owo.util.pond.OwoCreativeInventoryScreenExtensions;\nimport me.shedaniel.math.Rectangle;\nimport me.shedaniel.rei.api.client.gui.drag.DraggableStack;\nimport me.shedaniel.rei.api.client.gui.drag.DraggableStackVisitor;\nimport me.shedaniel.rei.api.client.gui.drag.DraggedAcceptorResult;\nimport me.shedaniel.rei.api.client.gui.drag.DraggingContext;\nimport me.shedaniel.rei.api.client.plugins.REIClientPlugin;\nimport me.shedaniel.rei.api.client.registry.screen.ExclusionZones;\nimport me.shedaniel.rei.api.client.registry.screen.OverlayDecider;\nimport me.shedaniel.rei.api.client.registry.screen.OverlayRendererProvider;\nimport me.shedaniel.rei.api.client.registry.screen.ScreenRegistry;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Stream;\n\npublic class OwoReiPlugin implements REIClientPlugin {\n\n    @SuppressWarnings(\"UnstableApiUsage\")\n    private static @Nullable OverlayRendererProvider.Sink renderSink = null;\n\n    @Override\n    public void registerExclusionZones(ExclusionZones zones) {\n        zones.register(CreativeModeInventoryScreen.class, screen -> {\n            var group = CreativeModeInventoryScreenAccessor.owo$getSelectedTab();\n            if (!(group instanceof OwoItemGroup owoGroup)) return Collections.emptySet();\n            if (owoGroup.getButtons().isEmpty()) return Collections.emptySet();\n\n            int x = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootX();\n            int y = ((OwoCreativeInventoryScreenExtensions) screen).owo$getRootY();\n\n            int stackHeight = owoGroup.getButtonStackHeight();\n            y -= 13 * (stackHeight - 4);\n\n            final var rectangles = new ArrayList<Rectangle>();\n            for (int i = 0; i < owoGroup.getButtons().size(); i++) {\n                int xOffset = x + 198 + (i / stackHeight) * 26;\n                int yOffset = y + 10 + (i % stackHeight) * 30;\n                rectangles.add(new Rectangle(xOffset, yOffset, 24, 24));\n            }\n\n            return rectangles;\n        });\n\n        zones.register(BaseOwoContainerScreen.class, screen -> {\n            return ((BaseOwoContainerScreen<?, ?>) screen).componentsForExclusionAreas()\n                .map(rect -> new Rectangle(rect.x(), rect.y(), rect.width(), rect.height()))\n                .toList();\n        });\n\n        zones.register(BraidScreen.class, screen -> {\n            List<Rectangle> rectangles = new ArrayList<>();\n\n            var visitor = new WidgetInstance.Visitor() {\n                @Override\n                public void visit(WidgetInstance<?> child) {\n                    if (child instanceof RecipeViewerExclusionZone.Instance area) {\n                        var bounds = area.computeGlobalBounds();\n\n                        rectangles.add(new Rectangle(bounds.minX, bounds.minY, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY));\n                    }\n\n                    child.visitChildren(this);\n                }\n            };\n\n            screen.state.rootInstance().visitChildren(visitor);\n\n            return rectangles;\n        });\n    }\n\n    @Override\n    public void registerScreens(ScreenRegistry registry) {\n        registry.registerDecider(new OverlayDecider() {\n            @Override\n            public <R extends Screen> boolean isHandingScreen(Class<R> screen) {\n                return BaseOwoContainerScreen.class.isAssignableFrom(screen);\n            }\n\n            @Override\n            @SuppressWarnings(\"UnstableApiUsage\")\n            public OverlayRendererProvider getRendererProvider() {\n                return new OverlayRendererProvider() {\n                    @Override\n                    public void onApplied(Sink sink) {\n                        renderSink = sink;\n                    }\n\n                    @Override\n                    public void onRemoved() {\n                        renderSink = null;\n                    }\n                };\n            }\n        });\n\n        registry.registerFocusedStack((screen, mouse) -> {\n            if (!(screen instanceof BraidScreen braid)) return CompoundEventResult.pass();\n\n            var hit = braid.state.hitTest(mouse.x, mouse.y)\n                .firstWhere(x -> x.instance() instanceof RecipeViewerStack.Instance);\n\n            if (hit == null) return CompoundEventResult.pass();\n\n            var instance = (RecipeViewerStack.Instance) hit.instance();\n\n            return CompoundEventResult.interruptTrue(ReiStackUtil.toRei(instance.widget().stackProvider.get()));\n        });\n\n        registry.registerDraggableStackVisitor(new DraggableStackVisitor<Screen>() {\n            @Override\n            public <R extends Screen> boolean isHandingScreen(R screen) {\n                return screen instanceof BraidScreen;\n            }\n\n            @Override\n            public Stream<BoundsProvider> getDraggableAcceptingBounds(DraggingContext<Screen> context, DraggableStack stack) {\n                if (!(context.getScreen() instanceof BraidScreen braid)) return Stream.empty();\n\n                List<BoundsProvider> allBounds = new ArrayList<>();\n\n                var converted = ReiStackUtil.fromRei(stack.getStack());\n\n                var visitor = new WidgetInstance.Visitor() {\n                    @Override\n                    public void visit(WidgetInstance<?> child) {\n                        if (child instanceof StackDropArea.Instance area && area.widget().stackPredicate.test(converted)) {\n                            var bounds = area.computeGlobalBounds();\n\n                            allBounds.add(BoundsProvider.ofRectangle(new Rectangle(bounds.minX, bounds.minY, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY)));\n                        }\n\n                        child.visitChildren(this);\n                    }\n                };\n\n                braid.state.rootInstance().visitChildren(visitor);\n\n                return allBounds.stream();\n            }\n\n            @Override\n            public DraggedAcceptorResult acceptDraggedStack(DraggingContext<Screen> context, DraggableStack stack) {\n                if (!(context.getScreen() instanceof BraidScreen braid)) return DraggedAcceptorResult.PASS;\n\n                var hit = braid.state.hitTest(context.getCurrentPosition().x, context.getCurrentPosition().y)\n                    .firstWhere(x -> x.instance() instanceof StackDropArea.Instance);\n\n                if (hit == null) return DraggedAcceptorResult.PASS;\n\n                var instance = (StackDropArea.Instance) hit.instance();\n\n                var converted = ReiStackUtil.fromRei(stack.getStack());\n\n                if (!instance.widget().stackPredicate.test(converted)) return DraggedAcceptorResult.PASS;\n\n                instance.widget().stackAcceptor.accept(converted);\n\n                return DraggedAcceptorResult.ACCEPTED;\n            }\n        });\n    }\n\n//    static {\n//        ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {\n//            if (!(screen instanceof BaseOwoHandledScreenAccessor accessor)) return;\n//\n//            ScreenEvents.beforeRender(screen).register(($, context, mouseX, mouseY, tickDelta) -> {\n//                var root = accessor.owo$getUIAdapter().rootComponent;\n//\n//                CallbackSurface surface;\n//                if (root.surface() instanceof CallbackSurface wrapped) {\n//                    surface = wrapped;\n//                } else {\n//                    surface = new CallbackSurface(root.surface());\n//                    root.surface(surface);\n//                }\n//\n//                surface.callback = () -> {\n//                    if (renderSink == null) return;\n//                    renderOverlay($, () -> renderSink.render(context, mouseX, mouseY, tickDelta));\n//                };\n//            });\n//\n//            ScreenEvents.afterRender(screen).register(($, matrices, mouseX, mouseY, tickDelta) -> {\n//                if (renderSink == null) return;\n//                renderOverlay($, () -> renderSink.lateRender(matrices, mouseX, mouseY, tickDelta));\n//            });\n//        });\n//    }\n//\n//    private static void renderOverlay(Screen screen, Runnable renderFunction) {\n//        if (REIRuntime.getInstance().getSearchTextField().getText().equals(\"froge\")) {\n//            var modelView = RenderSystem.getModelViewStack();\n//\n//            final var time = System.currentTimeMillis();\n//            float scale = .75f + (float) (Math.sin(time / 500d) * .5f);\n//            modelView.pushMatrix();\n//            modelView.translate(screen.width / 2f - scale / 2f * screen.width, screen.height / 2f - scale / 2f * screen.height, 0);\n//            modelView.scale(scale, scale, 1f);\n//            modelView.translate((float) (Math.sin(time / 1000d) * .75f) * screen.width, (float) (Math.sin(time / 500d) * .75f) * screen.height, 0);\n//\n//            modelView.translate(screen.width / 2f, screen.height / 2f, 0);\n//            modelView.rotate(RotationAxis.POSITIVE_Z.rotationDegrees((float) (time / 25d % 360d)));\n//            modelView.translate(screen.width / -2f, screen.height / -2f, 0);\n//\n//            for (int i = 0; i < 20; i++) {\n//                modelView.pushMatrix();\n//                modelView.translate(screen.width / 2f, screen.height / 2f, 0);\n//                modelView.rotate(RotationAxis.POSITIVE_Z.rotationDegrees(i * 18));\n//                modelView.translate(screen.width / -2f, screen.height / -2f, 0);\n//\n//                ScissorStack.pushDirect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);\n//                renderFunction.run();\n//                GlStateManager._enableScissorTest();\n//                ScissorStack.pop();\n//                modelView.popMatrix();\n//            }\n//\n//            modelView.popMatrix();\n//        } else {\n//            ScissorStack.pushDirect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);\n//            renderFunction.run();\n//            GlStateManager._enableScissorTest();\n//            ScissorStack.pop();\n//        }\n//    }\n//\n//    private static class CallbackSurface implements Surface {\n//        public final Surface inner;\n//        public @NotNull Runnable callback = () -> {};\n//\n//        private CallbackSurface(Surface inner) {\n//            this.inner = inner;\n//        }\n//\n//        @Override\n//        public void draw(OwoUIDrawContext context, ParentComponent component) {\n//            this.inner.draw(context, component);\n//            this.callback.run();\n//        }\n//    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/compat/rei/ReiStackUtil.java",
    "content": "package io.wispforest.owo.compat.rei;\n\nimport dev.architectury.fluid.FluidStack;\nimport dev.architectury.hooks.fluid.fabric.FluidStackHooksFabric;\nimport io.wispforest.owo.util.ViewerStack;\nimport me.shedaniel.rei.api.common.entry.EntryStack;\nimport me.shedaniel.rei.api.common.util.EntryStacks;\nimport net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;\nimport net.minecraft.world.item.ItemStack;\n\npublic class ReiStackUtil {\n    public static ViewerStack fromRei(EntryStack<?> stack) {\n        if (stack.getValue() instanceof ItemStack item) {\n            return ViewerStack.OfItem.of(item);\n        } else if (stack.getValue() instanceof FluidStack fluid) {\n            return new ViewerStack.OfFluid(FluidVariant.of(fluid.getFluid(), fluid.getPatch()), fluid.getAmount());\n        } else {\n            // TODO: custom REI stack.\n            return ViewerStack.OfItem.EMPTY;\n        }\n    }\n\n    public static EntryStack<?> toRei(ViewerStack stack) {\n        if (stack instanceof ViewerStack.OfItem ofItem) {\n            return EntryStacks.of(ofItem.asStack());\n        } else if (stack instanceof ViewerStack.OfFluid ofFluid) {\n            return EntryStacks.of(FluidStackHooksFabric.fromFabric(ofFluid.fluid(), ofFluid.count()));\n        } else {\n            throw new IllegalStateException(\"Invalid ViewerStack\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/compat/rei/ReiUIAdapter.java",
    "content": "package io.wispforest.owo.compat.rei;\n\nimport io.wispforest.owo.ui.core.OwoUIAdapter;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Sizing;\nimport me.shedaniel.math.Point;\nimport me.shedaniel.math.Rectangle;\nimport me.shedaniel.rei.api.client.gui.widgets.Widget;\nimport me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\n\nimport java.util.List;\nimport java.util.function.BiFunction;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\npublic class ReiUIAdapter<T extends ParentUIComponent> extends Widget {\n\n    public static final Point LAYOUT = new Point(-69, -69);\n\n    public final OwoUIAdapter<T> adapter;\n\n    public ReiUIAdapter(Rectangle bounds, BiFunction<Sizing, Sizing, T> rootComponentMaker) {\n        this.adapter = OwoUIAdapter.createWithoutScreen(bounds.x, bounds.y, bounds.width, bounds.height, rootComponentMaker);\n        this.adapter.inspectorZOffset = 900;\n\n        var screenWithREI = Minecraft.getInstance().screen;\n\n        if (screenWithREI != null) {\n            ScreenEvents.remove(screenWithREI).register(screen -> this.adapter.dispose());\n            ScreenEvents.afterRender(screenWithREI).register((screen, drawContext, mouseX, mouseY, tickDelta) -> {\n                this.adapter.drawTooltip(drawContext, mouseX, mouseY, tickDelta);\n            });\n        }\n    }\n\n    public void prepare() {\n        this.adapter.inflateAndMount();\n    }\n\n    public T rootComponent() {\n        return this.adapter.rootComponent;\n    }\n\n    public <W extends WidgetWithBounds> ReiWidgetComponent wrap(W widget) {\n        return new ReiWidgetComponent(widget);\n    }\n\n    public <W extends WidgetWithBounds> ReiWidgetComponent wrap(Function<Point, W> widgetFactory, Consumer<W> widgetConfigurator) {\n        var widget = widgetFactory.apply(LAYOUT);\n        widgetConfigurator.accept(widget);\n        return new ReiWidgetComponent(widget);\n    }\n\n    @Override\n    public boolean containsMouse(double mouseX, double mouseY) {\n        return this.adapter.isMouseOver(mouseX, mouseY);\n    }\n\n    @Override\n    public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {\n        return this.adapter.mouseClicked(new MouseButtonEvent(click.x() - this.adapter.x(), click.y() - this.adapter.y(), click.buttonInfo()), doubled);\n    }\n\n    @Override\n    public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {\n        return this.adapter.mouseScrolled(mouseX - this.adapter.x(), mouseY - this.adapter.y(), horizontalAmount, verticalAmount);\n    }\n\n    @Override\n    public boolean mouseReleased(MouseButtonEvent click) {\n        return this.adapter.mouseReleased(new MouseButtonEvent(click.x() - this.adapter.x(), click.y() - this.adapter.y(), click.buttonInfo()));\n    }\n\n    @Override\n    public boolean mouseDragged(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.adapter.mouseDragged(new MouseButtonEvent(click.x() - this.adapter.x(), click.y() - this.adapter.y(), click.buttonInfo()), deltaX, deltaY);\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        return this.adapter.keyPressed(input);\n    }\n\n    @Override\n    public boolean keyReleased(KeyEvent input) {\n        return this.adapter.keyReleased(input);\n    }\n\n    @Override\n    public boolean charTyped(CharacterEvent input) {\n        return this.adapter.charTyped(input);\n    }\n\n    @Override\n    public void render(GuiGraphics context, int mouseX, int mouseY, float partialTicks) {\n        context.enableScissor(this.adapter.x(), this.adapter.y(), this.adapter.width(), this.adapter.height());\n        this.adapter.render(context, mouseX, mouseY, partialTicks);\n        context.disableScissor();\n    }\n\n    @Override\n    public List<? extends GuiEventListener> children() {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/compat/rei/ReiWidgetComponent.java",
    "content": "package io.wispforest.owo.compat.rei;\n\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Sizing;\nimport me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\n\npublic class ReiWidgetComponent extends BaseUIComponent {\n\n    private final WidgetWithBounds widget;\n\n    protected ReiWidgetComponent(WidgetWithBounds widget) {\n        this.widget = widget;\n\n        var bounds = widget.getBounds();\n        this.horizontalSizing.set(Sizing.fixed(bounds.getWidth()));\n        this.verticalSizing.set(Sizing.fixed(bounds.getHeight()));\n\n        this.mouseEnter().subscribe(() -> {\n            this.focusHandler().focus(this, FocusSource.KEYBOARD_CYCLE);\n        });\n\n        this.mouseLeave().subscribe(() -> {\n            this.focusHandler().focus(null, null);\n        });\n    }\n\n    @Override\n    public void mount(ParentUIComponent parent, int x, int y) {\n        super.mount(parent, x, y);\n        this.applyToWidget();\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        this.widget.render(graphics, mouseX, mouseY, partialTicks);\n    }\n\n    @Override\n    public void drawFocusHighlight(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {}\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.widget.getBounds().getWidth();\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return this.widget.getBounds().getHeight();\n    }\n\n    @Override\n    public void updateX(int x) {\n        super.updateX(x);\n        this.applyToWidget();\n    }\n\n    @Override\n    public void updateY(int y) {\n        super.updateY(y);\n        this.applyToWidget();\n    }\n\n    private void applyToWidget() {\n        var bounds = this.widget.getBounds();\n\n        bounds.x = this.x;\n        bounds.y = this.y;\n\n        bounds.width = this.width;\n        bounds.height = this.height;\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        return this.widget.mouseClicked(new MouseButtonEvent(this.x + click.x(), this.y + click.y(), click.buttonInfo()), doubled)\n                | super.onMouseDown(click, doubled);\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        return this.widget.mouseReleased(new MouseButtonEvent(this.x + click.x(), this.y + click.y(), click.buttonInfo()))\n                | super.onMouseUp(click);\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        return this.widget.mouseScrolled(this.x + mouseX, this.y + mouseY, 0, amount)\n                | super.onMouseScroll(mouseX, mouseY, amount);\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.widget.mouseDragged(new MouseButtonEvent(this.x + click.x(), this.y + click.y(), click.buttonInfo()), deltaX, deltaY)\n                | super.onMouseDrag(click, deltaX, deltaY);\n    }\n\n    @Override\n    public boolean onCharTyped(CharacterEvent input) {\n        return this.widget.charTyped(input)\n                | super.onCharTyped(input);\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        return this.widget.keyPressed(input)\n                | super.onKeyPress(input);\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ConfigAP.java",
    "content": "package io.wispforest.owo.config;\n\nimport io.wispforest.owo.config.annotation.Config;\nimport io.wispforest.owo.config.annotation.Hook;\nimport io.wispforest.owo.config.annotation.Nest;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.NotNull;\n\nimport javax.annotation.processing.*;\nimport javax.lang.model.SourceVersion;\nimport javax.lang.model.element.ElementKind;\nimport javax.lang.model.element.TypeElement;\nimport javax.lang.model.type.DeclaredType;\nimport javax.lang.model.type.TypeKind;\nimport javax.lang.model.type.TypeMirror;\nimport javax.tools.Diagnostic;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.util.*;\n\n@ApiStatus.Internal\n@SupportedAnnotationTypes(\"io.wispforest.owo.config.annotation.Config\")\n@SupportedSourceVersion(SourceVersion.RELEASE_17)\npublic class ConfigAP extends AbstractProcessor {\n\n    private static final String WRAPPER_TEMPLATE = \"\"\"\n            package {package};\n\n            import blue.endless.jankson.Jankson;\n            import io.wispforest.owo.config.ConfigWrapper;\n            import io.wispforest.owo.config.ConfigWrapper.BuilderConsumer;\n            import io.wispforest.owo.config.Option;\n            import io.wispforest.owo.util.Observable;\n\n            import java.util.HashMap;\n            import java.util.Map;\n            import java.util.function.Consumer;\n\n            public class {wrapper_class_name} extends ConfigWrapper<{config_class_name}> {\n\n                public final Keys keys = new Keys();\n\n            {option_instances}\n\n                private {wrapper_class_name}() {\n                    super({config_class_name}.class);\n                }\n\n                private {wrapper_class_name}(BuilderConsumer consumer) {\n                    super({config_class_name}.class, consumer);\n                }\n\n                public static {wrapper_class_name} createAndLoad() {\n                    var wrapper = new {wrapper_class_name}();\n                    wrapper.load();\n                    return wrapper;\n                }\n\n                public static {wrapper_class_name} createAndLoad(BuilderConsumer consumer) {\n                    var wrapper = new {wrapper_class_name}(consumer);\n                    wrapper.load();\n                    return wrapper;\n                }\n\n            {accessors}\n\n            {type_interfaces}\n\n                public static class Keys {\n            {key_constants}\n                }\n            }\n            \"\"\";\n\n    private static final String GET_ACCESSOR_TEMPLATE = \"\"\"\n            public {field_type} {field_name}() {\n                return {option_instance}.value();\n            }\n            \"\"\";\n\n    private static final String SET_ACCESSOR_TEMPLATE = \"\"\"\n            public void {field_name}({field_type} value) {\n                {option_instance}.set(value);\n            }\n            \"\"\";\n\n    private static final String SUBSCRIBE_TEMPLATE = \"\"\"\n            public void subscribeTo{field_name}(Consumer<{field_type}> subscriber) {\n                {option_instance}.observe(subscriber);\n            }\n            \"\"\";\n\n    private final Set<TypeElement> nestTypes = new LinkedHashSet<>();\n    private Map<TypeMirror, TypeMirror> primitivesToWrappers;\n\n    @Override\n    public synchronized void init(ProcessingEnvironment processingEnv) {\n        super.init(processingEnv);\n\n        final var typeUtils = processingEnv.getTypeUtils();\n        final var elementUtils = processingEnv.getElementUtils();\n\n        this.primitivesToWrappers = Map.of(\n                typeUtils.getPrimitiveType(TypeKind.BYTE), elementUtils.getTypeElement(\"java.lang.Byte\").asType(),\n                typeUtils.getPrimitiveType(TypeKind.CHAR), elementUtils.getTypeElement(\"java.lang.Character\").asType(),\n                typeUtils.getPrimitiveType(TypeKind.SHORT), elementUtils.getTypeElement(\"java.lang.Short\").asType(),\n                typeUtils.getPrimitiveType(TypeKind.INT), elementUtils.getTypeElement(\"java.lang.Integer\").asType(),\n                typeUtils.getPrimitiveType(TypeKind.LONG), elementUtils.getTypeElement(\"java.lang.Long\").asType(),\n                typeUtils.getPrimitiveType(TypeKind.FLOAT), elementUtils.getTypeElement(\"java.lang.Float\").asType(),\n                typeUtils.getPrimitiveType(TypeKind.DOUBLE), elementUtils.getTypeElement(\"java.lang.Double\").asType(),\n                typeUtils.getPrimitiveType(TypeKind.BOOLEAN), elementUtils.getTypeElement(\"java.lang.Boolean\").asType()\n        );\n    }\n\n    @Override\n    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {\n        for (var annotation : annotations) {\n            var annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);\n            for (var annotated : annotatedElements) {\n                if (annotated.getKind() != ElementKind.CLASS) continue;\n\n                var clazz = (TypeElement) annotated;\n                var className = clazz.getQualifiedName().toString();\n                var wrapperName = annotated.getAnnotation(Config.class).wrapperName();\n\n                try {\n                    var file = this.processingEnv.getFiler().createSourceFile(wrapperName);\n                    try (var writer = new PrintWriter(file.openWriter())) {\n                        writer.println(makeWrapper(wrapperName, className, this.collectFields(Option.Key.ROOT, clazz, clazz.getAnnotation(Config.class).defaultHook())));\n                    }\n                } catch (IOException e) {\n                    throw new RuntimeException(\"Failed to generate config wrapper\", e);\n                }\n            }\n        }\n\n        return true;\n    }\n\n    private List<ConfigField> collectFields(Option.Key parent, TypeElement clazz, boolean defaultHook) {\n        var messager = this.processingEnv.getMessager();\n        var list = new ArrayList<ConfigField>();\n\n        for (var field : clazz.getEnclosedElements()) {\n            if (field.getKind() != ElementKind.FIELD) continue;\n\n            var fieldType = field.asType();\n            var fieldName = field.getSimpleName().toString();\n\n            if (fieldType.getKind() == TypeKind.TYPEVAR) {\n                messager.printMessage(Diagnostic.Kind.ERROR, \"Generic field types are not allowed in config classes\");\n            }\n\n            TypeElement typeElement = null;\n            if (fieldType.getKind() == TypeKind.DECLARED) {\n                typeElement = (TypeElement) ((DeclaredType) fieldType).asElement();\n\n                if (typeElement == clazz) {\n                    messager.printMessage(Diagnostic.Kind.ERROR, \"Illegal self-reference in nested config object\");\n                }\n            }\n\n            if (typeElement != null && field.getAnnotation(Nest.class) != null) {\n                this.nestTypes.add(typeElement);\n                list.add(new NestField(fieldName, collectFields(parent.child(fieldName), typeElement, defaultHook), typeElement.getSimpleName().toString()));\n            } else {\n                list.add(new ValueField(fieldName, parent.child(fieldName), field.asType(),\n                        defaultHook || field.getAnnotation(Hook.class) != null));\n            }\n        }\n\n        return list;\n    }\n\n    private String makeWrapper(String wrapperClassName, String configClassName, List<ConfigField> fields) {\n        var baseWrapper = WRAPPER_TEMPLATE\n                .replace(\"{wrapper_class_name}\", wrapperClassName)\n                .replace(\"{package}\", configClassName.substring(0, configClassName.lastIndexOf(\".\")))\n                .replace(\"{config_class_name}\", configClassName);\n\n        var accessorMethods = new Writer(new StringBuilder());\n        var optionInstances = new Writer(new StringBuilder());\n        var keyConstants = new Writer(new StringBuilder());\n        var typeInterfaces = new Writer(new StringBuilder());\n\n        for (var nestType : this.nestTypes) {\n            typeInterfaces.beginLine(\"public interface \").write(nestType.getSimpleName().toString()).endLine(\" {\");\n            typeInterfaces.beginBlock();\n            for (var enclosed : nestType.getEnclosedElements()) {\n                if (enclosed.getKind() != ElementKind.FIELD) continue;\n                if (enclosed.getAnnotation(Nest.class) != null) continue;\n\n                typeInterfaces.beginLine(enclosed.asType().toString()).write(\" \").write(enclosed.getSimpleName().toString()).endLine(\"();\");\n                typeInterfaces.beginLine(\"void \").write(enclosed.getSimpleName().toString()).write(\"(\").write(enclosed.asType().toString()).endLine(\" value);\");\n            }\n            typeInterfaces.endBlock();\n            typeInterfaces.line(\"}\");\n        }\n\n        keyConstants.beginBlock();\n        for (var field : fields) {\n            field.appendAccessors(accessorMethods, optionInstances, keyConstants);\n        }\n\n        return baseWrapper\n                .replace(\"{option_instances}\", optionInstances.finish())\n                .replace(\"{type_interfaces}\\n\", typeInterfaces.finish())\n                .replace(\"{key_constants}\", keyConstants.finish())\n                .replace(\"{accessors}\\n\", accessorMethods.finish());\n    }\n\n    private String makeGetAccessor(String fieldName, Option.Key fieldKey, TypeMirror fieldType) {\n        return GET_ACCESSOR_TEMPLATE\n                .replace(\"{option_instance}\", constantNameOf(fieldKey))\n                .replace(\"{field_name}\", fieldName)\n                .replace(\"{field_type}\", fieldType.toString());\n    }\n\n    private String makeSetAccessor(String fieldName, Option.Key fieldKey, TypeMirror fieldType) {\n        return SET_ACCESSOR_TEMPLATE\n                .replace(\"{option_instance}\", constantNameOf(fieldKey))\n                .replace(\"{field_name}\", fieldName)\n                .replace(\"{field_type}\", fieldType.toString());\n    }\n\n    private String makeSubscribe(String fieldName, Option.Key fieldKey, TypeMirror fieldType) {\n        return SUBSCRIBE_TEMPLATE\n                .replace(\"{option_instance}\", constantNameOf(fieldKey))\n                .replace(\"{field_name}\", fieldName)\n                .replace(\"{field_type}\", this.primitivesToWrappers.getOrDefault(fieldType, fieldType).toString());\n    }\n\n    private String constantNameOf(Option.Key key) {\n        return key.asString().replace(\".\", \"_\");\n    }\n\n    private interface ConfigField {\n        void appendAccessors(Writer accessors, Writer optionInstances, Writer keyConstants);\n    }\n\n    private final class ValueField implements ConfigField {\n        private final String name;\n        private final Option.Key key;\n        private final TypeMirror type;\n        private final boolean makeSubscribe;\n\n        private ValueField(String name, Option.Key key, TypeMirror type, boolean makeSubscribe) {\n            this.name = name;\n            this.key = key;\n            this.type = type;\n            this.makeSubscribe = makeSubscribe;\n        }\n\n        @Override\n        public void appendAccessors(Writer accessors, Writer optionInstances, Writer keyConstants) {\n            keyConstants.line(\"public final Option.Key \" + constantNameOf(this.key) + \" = new Option.Key(\\\"\" + this.key.asString() + \"\\\");\");\n            optionInstances.line(\"private final Option<\" + primitivesToWrappers.getOrDefault(type, type) + \"> \" + constantNameOf(this.key) + \" = this.optionForKey(this.keys.\" + constantNameOf(this.key) + \");\");\n\n            accessors.append(makeGetAccessor(this.name, this.key, this.type)).write(\"\\n\");\n            accessors.append(makeSetAccessor(this.name, this.key, this.type)).write(\"\\n\");\n            if (this.makeSubscribe) accessors.append(makeSubscribe(capitalize(this.name), this.key, this.type)).write(\"\\n\");\n        }\n    }\n\n    private record NestField(String nestName, List<ConfigField> children, String typeName) implements ConfigField {\n        @Override\n        public void appendAccessors(Writer accessors, Writer optionInstances, Writer keyConstants) {\n            var nestClassName = capitalize(nestName);\n            if (nestClassName.equals(typeName)) nestClassName += \"_\";\n\n            // TODO replace type interface with class and instantiate instead of one class per field\n\n            accessors.beginLine(\"public final \").write(nestClassName).write(\" \").write(nestName).write(\" = new \").write(nestClassName).endLine(\"();\");\n            accessors.beginLine(\"public class \").write(nestClassName).write(\" implements \").write(typeName).endLine(\" {\");\n            accessors.beginBlock();\n            for (var child : children) {\n                child.appendAccessors(accessors, optionInstances, keyConstants);\n            }\n            accessors.endBlock();\n            accessors.line(\"}\");\n        }\n    }\n\n    private static String capitalize(String string) {\n        return string.substring(0, 1).toUpperCase(Locale.ROOT) + string.substring(1);\n    }\n\n    private static class Writer implements CharSequence {\n\n        private final StringBuilder builder;\n        private int indentLevel = 1;\n\n        private Writer(StringBuilder builder) {\n            this.builder = builder;\n        }\n\n        public Writer beginLine(CharSequence text) {\n            this.builder.append(\" \".repeat(this.indentLevel * 4)).append(text);\n            return this;\n        }\n\n        public void endLine(CharSequence text) {\n            this.builder.append(text).append(\"\\n\");\n        }\n\n        public void line(CharSequence text) {\n            this.builder.append(\"    \".repeat(this.indentLevel)).append(text).append(\"\\n\");\n        }\n\n        public Writer append(String text) {\n            for (var line : text.split(\"\\n\")) {\n                this.line(line);\n            }\n\n            return this;\n        }\n\n        public Writer write(CharSequence text) {\n            this.builder.append(text);\n            return this;\n        }\n\n        public void beginBlock() {\n            this.indentLevel++;\n        }\n\n        public void endBlock() {\n            this.indentLevel--;\n        }\n\n        public String finish() {\n            if (this.builder.isEmpty()) return \"\";\n            if (this.builder.charAt(builder.length() - 1) == '\\n') {\n                this.builder.deleteCharAt(this.builder.length() - 1);\n            }\n\n            return this.builder.toString();\n        }\n\n        @Override\n        public int length() {\n            return this.builder.length();\n        }\n\n        @Override\n        public char charAt(int index) {\n            return this.builder.charAt(index);\n        }\n\n        @Override\n        public @NotNull CharSequence subSequence(int start, int end) {\n            return this.builder.subSequence(start, end);\n        }\n\n        @Override\n        public @NotNull String toString() {\n            return this.builder.toString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ConfigSynchronizer.java",
    "content": "package io.wispforest.owo.config;\n\nimport com.google.common.collect.HashMultimap;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.ServerCommonPacketListenerImplAccessor;\nimport io.wispforest.owo.ops.TextOps;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;\nimport net.fabricmc.fabric.api.event.Event;\nimport net.fabricmc.fabric.api.networking.v1.PacketByteBufs;\nimport net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.network.Connection;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.util.Tuple;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.WeakHashMap;\nimport java.util.function.BiConsumer;\n\npublic class ConfigSynchronizer {\n\n    public static final Identifier CONFIG_SYNC_CHANNEL = Owo.id(\"config_sync\");\n\n    private static final Map<Connection, Map<String, Map<Option.Key, Object>>> CLIENT_OPTION_STORAGE = new WeakHashMap<>();\n\n    private static final Map<String, ConfigWrapper<?>> KNOWN_CONFIGS = new HashMap<>();\n    private static final MutableComponent PREFIX = TextOps.concat(Owo.PREFIX, Component.nullToEmpty(\"§cunrecoverable config mismatch\\n\\n\"));\n\n    static void register(ConfigWrapper<?> config) {\n        KNOWN_CONFIGS.put(config.name(), config);\n    }\n\n    /**\n     * Retrieve the options which the given player's client\n     * sent to the server during config synchronization\n     *\n     * @param player     The player for which to retrieve the client values\n     * @param configName The name of the config for which to retrieve values\n     * @return The player's client's values of the given config options,\n     * or {@code null} if no config with the given name was synced\n     */\n    public static @Nullable Map<Option.Key, ?> getClientOptions(ServerPlayer player, String configName) {\n        var storage = CLIENT_OPTION_STORAGE.get(((ServerCommonPacketListenerImplAccessor) player.connection).owo$getConnection());\n        if (storage == null) return null;\n\n        return storage.get(configName);\n    }\n\n    /**\n     * Safer, more clear version of {@link #getClientOptions(ServerPlayer, String)} to\n     * be used when the actual config wrapper is available\n     *\n     * @see #getClientOptions(ServerPlayer, String)\n     */\n    public static @Nullable Map<Option.Key, ?> getClientOptions(ServerPlayer player, ConfigWrapper<?> config) {\n        return getClientOptions(player, config.name());\n    }\n\n    private static ConfigSyncPacket toPacket(Option.SyncMode targetMode) {\n        Map<String, ConfigEntry> configs = new HashMap<>();\n\n        KNOWN_CONFIGS.forEach((configName, config) -> {\n            var entry = new ConfigEntry(new HashMap<>());\n\n            config.allOptions().forEach((key, option) -> {\n                if (option.syncMode().ordinal() < targetMode.ordinal()) return;\n\n                FriendlyByteBuf optionBuf = PacketByteBufs.create();\n                option.write(optionBuf);\n\n                entry.options().put(key.asString(), optionBuf);\n            });\n\n            configs.put(configName, entry);\n        });\n\n        return new ConfigSyncPacket(configs);\n    }\n\n    private static void read(ConfigSyncPacket packet, BiConsumer<Option<?>, FriendlyByteBuf> optionConsumer) {\n        for (var configEntry : packet.configs().entrySet()) {\n            var configName = configEntry.getKey();\n            var config = KNOWN_CONFIGS.get(configName);\n            if (config == null) {\n                Owo.LOGGER.error(\"Received overrides for unknown config '{}', skipping\", configName);\n                continue;\n            }\n\n            for (var optionEntry : configEntry.getValue().options().entrySet()) {\n                var optionKey = new Option.Key(optionEntry.getKey());\n                var option = config.optionForKey(optionKey);\n                if (option == null) {\n                    Owo.LOGGER.error(\"Received override for unknown option '{}' in config '{}', skipping\", optionKey, configName);\n                    continue;\n                }\n\n                optionConsumer.accept(option, optionEntry.getValue());\n            }\n        }\n    }\n\n    @Environment(EnvType.CLIENT)\n    private static void applyClient(ConfigSyncPacket payload, ClientPlayNetworking.Context context) {\n        Owo.LOGGER.info(\"Applying server overrides\");\n        var mismatchedOptions = new HashMap<Option<?>, Object>();\n\n        if (!(context.client().hasSingleplayerServer() && context.client().getSingleplayerServer().isSingleplayer())) {\n            read(payload, (option, packetByteBuf) -> {\n                var mismatchedValue = option.read(packetByteBuf);\n                if (mismatchedValue != null) mismatchedOptions.put(option, mismatchedValue);\n            });\n\n            if (!mismatchedOptions.isEmpty()) {\n                Owo.LOGGER.error(\"Aborting connection, non-syncable config values were mismatched\");\n                mismatchedOptions.forEach((option, serverValue) -> {\n                    Owo.LOGGER.error(\"- Option {} in config '{}' has value '{}' but server requires '{}'\",\n                            option.key().asString(), option.configName(), option.value(), serverValue);\n                });\n\n                var errorMessage = Component.empty();\n                var optionsByConfig = HashMultimap.<String, Tuple<Option<?>, Object>>create();\n\n                mismatchedOptions.forEach((option, serverValue) -> optionsByConfig.put(option.configName(), new Tuple<>(option, serverValue)));\n                for (var configName : optionsByConfig.keys()) {\n                    errorMessage.append(TextOps.withFormatting(\"in config \", ChatFormatting.GRAY)).append(configName).append(\"\\n\");\n                    for (var option : optionsByConfig.get(configName)) {\n                        errorMessage.append(Component.translatable(option.getA().translationKey()).withStyle(ChatFormatting.YELLOW)).append(\" -> \");\n                        errorMessage.append(option.getA().value().toString()).append(TextOps.withFormatting(\" (client)\", ChatFormatting.GRAY));\n                        errorMessage.append(TextOps.withFormatting(\" / \", ChatFormatting.DARK_GRAY));\n                        errorMessage.append(option.getB().toString()).append(TextOps.withFormatting(\" (server)\", ChatFormatting.GRAY)).append(\"\\n\");\n                    }\n                    errorMessage.append(\"\\n\");\n                }\n\n                errorMessage.append(TextOps.withFormatting(\"these options could not be synchronized because\\n\", ChatFormatting.GRAY));\n                errorMessage.append(TextOps.withFormatting(\"they require your client to be restarted\\n\", ChatFormatting.GRAY));\n                errorMessage.append(TextOps.withFormatting(\"change them manually and restart if you want to join this server\", ChatFormatting.GRAY));\n\n                context.player().connection.getConnection().disconnect(TextOps.concat(PREFIX, errorMessage));\n                return;\n            }\n        }\n\n        Owo.LOGGER.info(\"Responding with client values\");\n        context.responseSender().sendPacket(toPacket(Option.SyncMode.INFORM_SERVER));\n    }\n\n    private static void applyServer(ConfigSyncPacket payload, ServerPlayNetworking.Context context) {\n        Owo.LOGGER.info(\"Receiving client config\");\n        var connection = ((ServerCommonPacketListenerImplAccessor) context.player().connection).owo$getConnection();\n\n        read(payload, (option, optionBuf) -> {\n            var config = CLIENT_OPTION_STORAGE.computeIfAbsent(connection, $ -> new HashMap<>()).computeIfAbsent(option.configName(), s -> new HashMap<>());\n            config.put(option.key(), optionBuf.read(option.endec()));\n        });\n    }\n\n    private record ConfigSyncPacket(Map<String, ConfigEntry> configs) implements CustomPacketPayload {\n        public static final Type<ConfigSyncPacket> ID = new Type<>(CONFIG_SYNC_CHANNEL);\n        public static final Endec<ConfigSyncPacket> ENDEC = StructEndecBuilder.of(\n                ConfigEntry.ENDEC.mapOf().fieldOf(\"configs\", ConfigSyncPacket::configs),\n                ConfigSyncPacket::new\n        );\n\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return ID;\n        }\n    }\n\n    private record ConfigEntry(Map<String, FriendlyByteBuf> options) {\n        public static final Endec<ConfigEntry> ENDEC = StructEndecBuilder.of(\n                MinecraftEndecs.FRIENDLY_BYTE_BUF.mapOf().fieldOf(\"options\", ConfigEntry::options),\n                ConfigEntry::new\n        );\n    }\n\n    static {\n        var packetCodec = CodecUtils.toPacketCodec(ConfigSyncPacket.ENDEC);\n\n        PayloadTypeRegistry.playS2C().register(ConfigSyncPacket.ID, packetCodec);\n        PayloadTypeRegistry.playC2S().register(ConfigSyncPacket.ID, packetCodec);\n\n        var earlyPhase = Owo.id(\"early\");\n        ServerPlayConnectionEvents.JOIN.addPhaseOrdering(earlyPhase, Event.DEFAULT_PHASE);\n        ServerPlayConnectionEvents.JOIN.register(earlyPhase, (handler, sender, server) -> {\n            Owo.LOGGER.info(\"Sending server config values to client\");\n\n            sender.sendPacket(toPacket(Option.SyncMode.OVERRIDE_CLIENT));\n        });\n\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {\n            ClientPlayNetworking.registerGlobalReceiver(ConfigSyncPacket.ID, ConfigSynchronizer::applyClient);\n\n            ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {\n                KNOWN_CONFIGS.forEach((name, config) -> config.forEachOption(Option::reattach));\n            });\n        }\n\n        ServerPlayNetworking.registerGlobalReceiver(ConfigSyncPacket.ID, ConfigSynchronizer::applyServer);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ConfigWrapper.java",
    "content": "package io.wispforest.owo.config;\n\nimport blue.endless.jankson.Jankson;\nimport blue.endless.jankson.JsonElement;\nimport blue.endless.jankson.JsonGrammar;\nimport blue.endless.jankson.JsonPrimitive;\nimport blue.endless.jankson.api.DeserializationException;\nimport blue.endless.jankson.api.SyntaxError;\nimport blue.endless.jankson.impl.POJODeserializer;\nimport blue.endless.jankson.magic.TypeMagic;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.format.jankson.JanksonDeserializer;\nimport io.wispforest.endec.format.jankson.JanksonSerializer;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.config.annotation.*;\nimport io.wispforest.owo.config.ui.ConfigScreen;\nimport io.wispforest.owo.config.ui.ConfigScreenProviders;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.util.NumberReflection;\nimport io.wispforest.owo.util.Observable;\nimport io.wispforest.owo.util.ReflectionUtils;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.io.IOException;\nimport java.lang.invoke.MethodHandle;\nimport java.lang.invoke.MethodHandles;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport java.util.regex.Pattern;\n\n/**\n * The common base class of all generated config classes.\n * The majority of all config functionality resides in here\n * <p>\n * Do not extend this class yourself - instead annotate\n * a class describing your config model with {@link Config},\n * just as you would do with other libraries like Cloth Config\n *\n * @see Config\n */\npublic abstract class ConfigWrapper<C> {\n\n    private static final Map<String, Class<?>> KNOWN_CONFIG_CLASSES = new HashMap<>();\n\n    protected final String name;\n    protected final C instance;\n\n    protected boolean loading = false;\n    protected final Jankson jankson;\n\n    @SuppressWarnings(\"rawtypes\") protected final Map<Option.Key, Option> options = new LinkedHashMap<>();\n    @SuppressWarnings(\"rawtypes\") protected final Map<Option.Key, Option> optionsView = Collections.unmodifiableMap(options);\n\n    protected final ReflectiveEndecBuilder builder;\n\n    @Deprecated\n    protected ConfigWrapper(Class<C> clazz, Consumer<Jankson.Builder> janksonBuilder) {\n        this(clazz, (SerializationBuilder serializationBuilder) -> janksonBuilder.accept(serializationBuilder.janksonBuilder()));\n    }\n\n    protected ConfigWrapper(Class<C> clazz) {\n        this(clazz, (SerializationBuilder builder) -> {});\n    }\n\n    protected ConfigWrapper(Class<C> clazz, BuilderConsumer consumer) {\n        this.builder = MinecraftEndecs.addDefaults(new ReflectiveEndecBuilder());\n\n        ReflectionUtils.requireZeroArgsConstructor(clazz, s -> \"Config model class \" + s + \" must provide a zero-args constructor\");\n        this.instance = ReflectionUtils.tryInstantiateWithNoArgs(clazz);\n\n        var janksonBuilder = Jankson.builder();\n\n        var builder = new SerializationBuilder(janksonBuilder, this.builder);\n\n        builder.janksonBuilder()\n                .registerSerializer(Identifier.class, (identifier, marshaller) -> new JsonPrimitive(identifier.toString()))\n                .registerDeserializer(JsonPrimitive.class, Identifier.class, (primitive, m) -> Identifier.tryParse(primitive.asString()));\n\n        builder.addEndec(Color.class, Color.RGBA_HEX_ENDEC);\n\n        consumer.build(builder);\n\n        this.jankson = janksonBuilder.build();\n\n        var configAnnotation = clazz.getAnnotation(Config.class);\n        this.name = configAnnotation.name();\n\n        if (KNOWN_CONFIG_CLASSES.put(this.name, this.getClass()) != null) {\n            throw new IllegalStateException(\"Config name '\" + this.name + \"'\"\n                    + \" is already taken by an instance of class '\" + KNOWN_CONFIG_CLASSES.get(this.name).getName() + \"'\");\n        }\n\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT && clazz.isAnnotationPresent(Modmenu.class)) {\n            var modmenuAnnotation = clazz.getAnnotation(Modmenu.class);\n            ConfigScreenProviders.register(\n                    modmenuAnnotation.modId(),\n                    screen -> ConfigScreen.createWithCustomModel(Identifier.parse(modmenuAnnotation.uiModelId()), this, screen)\n            );\n        }\n\n        try {\n            this.initializeOptions(configAnnotation.saveOnModification());\n            for (var option : this.options.values()) {\n                if (option.syncMode().isNone()) continue;\n\n                ConfigSynchronizer.register(this);\n                break;\n            }\n        } catch (IllegalAccessException | NoSuchMethodException e) {\n            throw new RuntimeException(\"Failed to initialize config \" + this.name, e);\n        }\n    }\n\n    /**\n     * Save the config represented by this wrapper\n     */\n    public void save() {\n        if (this.loading) return;\n\n        try {\n            this.fileLocation().getParent().toFile().mkdirs();\n            Files.writeString(this.fileLocation(), this.jankson.toJson(this.instance).toJson(JsonGrammar.JANKSON), StandardCharsets.UTF_8);\n        } catch (IOException e) {\n            Owo.LOGGER.warn(\"Could not save config {}\", this.name, e);\n        }\n    }\n\n    /**\n     * Load the config represented by this wrapper from\n     * its associated file, or create it if it does not exist\n     */\n    @SuppressWarnings({\"unchecked\"})\n    public void load() {\n        if (!Files.exists(this.fileLocation())) {\n            this.save();\n            return;\n        }\n\n        try {\n            this.loading = true;\n            var configObject = this.jankson.load(Files.readString(this.fileLocation(), StandardCharsets.UTF_8));\n\n            for (var option : this.options.values()) {\n                Object newValue;\n\n                final var clazz = option.clazz();\n                final var element = configObject.recursiveGet(JsonElement.class, option.key().asString());\n                if (element == null) {\n                    option.set(option.defaultValue());\n                    continue;\n                }\n\n                if (Map.class.isAssignableFrom(clazz)) {\n                    var field = option.backingField().field();\n\n                    newValue = TypeMagic.createAndCast(clazz);\n                    POJODeserializer.unpackMap(\n                            (Map<Object, Object>) newValue,\n                            ReflectionUtils.getTypeArgument(field.getGenericType(), 0),\n                            ReflectionUtils.getTypeArgument(field.getGenericType(), 1),\n                            element,\n                            this.jankson.getMarshaller()\n                    );\n                } else if (List.class.isAssignableFrom(clazz) || Set.class.isAssignableFrom(clazz)) {\n                    newValue = TypeMagic.createAndCast(clazz);\n                    POJODeserializer.unpackCollection(\n                            (Collection<Object>) newValue,\n                            ReflectionUtils.getTypeArgument(option.backingField().field().getGenericType(), 0),\n                            element,\n                            this.jankson.getMarshaller()\n                    );\n                } else {\n                    newValue = configObject.getMarshaller().marshall(clazz, element);\n                }\n\n                if (!option.verifyConstraint(newValue)) continue;\n\n                option.set(newValue == null ? option.defaultValue() : newValue);\n            }\n        } catch (IOException | SyntaxError | DeserializationException e) {\n            Owo.LOGGER.warn(\"Could not load config {}\", this.name, e);\n        } finally {\n            this.loading = false;\n        }\n    }\n\n    /**\n     * Query the field associated with a given key. This is relevant\n     * in cases where said field is annotated with {@link Nest}, meaning\n     * that {@link #optionForKey(Option.Key)} would return {@code null}\n     * because the field won't be treated as an option in itself.\n     *\n     * @param key The for which to query the field\n     * @return The field described by {@code key}, or {@code null}\n     * if it does not point to a valid field in the config tree\n     */\n    public @Nullable Field fieldForKey(Option.Key key) {\n        try {\n            var path = new ArrayList<>(List.of(key.path()));\n            var clazz = this.instance.getClass();\n\n            while (path.size() > 1) {\n                clazz = clazz.getDeclaredField(path.remove(0)).getType();\n            }\n\n            return clazz.getField(path.get(0));\n        } catch (NoSuchFieldException e) {\n            return null;\n        }\n    }\n\n    /**\n     * @return The name of this config, used for translation\n     * keys and the filename\n     */\n    public String name() {\n        return this.name;\n    }\n\n    /**\n     * @return The location to which this config is saved\n     */\n    public Path fileLocation() {\n        return FabricLoader.getInstance().getConfigDir().resolve(this.name + \".json5\");\n    }\n\n    /**\n     * Query the config option associated with a given key\n     *\n     * @param key The key for which to query the option\n     * @return The option described by {@code key}, or {@code null}\n     * if no such option exists\n     */\n    @SuppressWarnings(\"unchecked\")\n    public <T> @Nullable Option<T> optionForKey(Option.Key key) {\n        return this.options.get(key);\n    }\n\n    /**\n     * @return A view of all options contained in this config\n     */\n    @SuppressWarnings(\"unchecked\")\n    public Map<Option.Key, Option<?>> allOptions() {\n        return (Map<Option.Key, Option<?>>) (Object) this.optionsView;\n    }\n\n    /**\n     * Execute the given action once for each option in this config\n     */\n    public void forEachOption(Consumer<Option<?>> action) {\n        for (var option : this.options.values()) {\n            action.accept(option);\n        }\n    }\n\n    private void initializeOptions(boolean hookSave) throws IllegalAccessException, NoSuchMethodException {\n        var fields = new LinkedHashMap<Option.Key, Option.BoundField<Object>>();\n        collectFieldValues(Option.Key.ROOT, this.instance, fields);\n\n        var instanceSyncMode = this.instance.getClass().isAnnotationPresent(Sync.class)\n                ? this.instance.getClass().getAnnotation(Sync.class).value()\n                : Option.SyncMode.NONE;\n\n        for (var entry : fields.entrySet()) {\n            var key = entry.getKey();\n            var boundField = entry.getValue();\n\n            var field = boundField.field();\n            var fieldType = field.getType();\n\n            Constraint constraint = null;\n            if (field.isAnnotationPresent(RangeConstraint.class)) {\n                var annotation = field.getAnnotation(RangeConstraint.class);\n\n                if (NumberReflection.isNumberType(fieldType)) {\n                    Predicate<?> predicate;\n                    if (fieldType == long.class || fieldType == Long.class) {\n                        predicate = o -> o != null && (Long) o >= annotation.min() && (Long) o <= annotation.max();\n                    } else {\n                        predicate = o -> o != null && ((Number) o).doubleValue() >= annotation.min() && ((Number) o).doubleValue() <= annotation.max();\n                    }\n\n                    constraint = new Constraint(\"Range from \" + annotation.min() + \" to \" + annotation.max(), predicate);\n                } else {\n                    throw new IllegalStateException(\"@RangeConstraint can only be applied to numeric fields\");\n                }\n            }\n\n            if (field.isAnnotationPresent(RegexConstraint.class)) {\n                var annotation = field.getAnnotation(RegexConstraint.class);\n\n                if (CharSequence.class.isAssignableFrom(fieldType)) {\n                    var pattern = Pattern.compile(annotation.value());\n                    constraint = new Constraint(\"Regex \" + annotation.value(), o -> o != null && pattern.matcher((CharSequence) o).matches());\n                } else {\n                    throw new IllegalStateException(\"@RegexConstraint can only be applied to fields with a string representation\");\n                }\n            }\n\n            if (field.isAnnotationPresent(PredicateConstraint.class)) {\n                var annotation = field.getAnnotation(PredicateConstraint.class);\n                var method = boundField.owner().getClass().getMethod(annotation.value(), fieldType);\n\n                if (method.getReturnType() != boolean.class) {\n                    throw new NoSuchMethodException(\"Return type of predicate implementation '\" + annotation.value() + \"' must be 'boolean'\");\n                }\n\n                if (!Modifier.isStatic(method.getModifiers())) {\n                    throw new IllegalStateException(\"Predicate implementation '\" + annotation.value() + \"' must be static\");\n                }\n\n                var handle = MethodHandles.publicLookup().unreflect(method);\n                constraint = new Constraint(\"Predicate method \" + annotation.value(), o -> this.invokePredicate(handle, o));\n            }\n\n            final var defaultValue = boundField.getValue();\n\n            final var observable = Observable.of(defaultValue);\n            if (hookSave) observable.observe(o -> this.save());\n\n            var syncMode = instanceSyncMode;\n            if (field.isAnnotationPresent(Sync.class)) {\n                syncMode = field.getAnnotation(Sync.class).value();\n            } else {\n                var parentKey = key.parent();\n                while (!parentKey.isRoot()) {\n                    var parentField = this.fieldForKey(parentKey);\n                    if (parentField.isAnnotationPresent(Sync.class)) {\n                        syncMode = parentField.getAnnotation(Sync.class).value();\n                    }\n\n                    parentKey = parentKey.parent();\n                }\n            }\n\n            this.options.put(key, new Option<>(this.name, key, defaultValue, observable, boundField, constraint, syncMode, this.builder));\n        }\n    }\n\n    private void collectFieldValues(Option.Key parent, Object instance, Map<Option.Key, Option.BoundField<Object>> fields) throws IllegalAccessException {\n        for (var field : instance.getClass().getDeclaredFields()) {\n            if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) continue;\n\n            if (field.isAnnotationPresent(Nest.class)) {\n                var fieldValue = field.get(instance);\n                if (fieldValue != null) {\n                    this.collectFieldValues(parent.child(field.getName()), fieldValue, fields);\n                } else {\n                    throw new IllegalStateException(\"Nested config option containers must never be null\");\n                }\n            } else {\n                fields.put(parent.child(field.getName()), new Option.BoundField<>(instance, field));\n            }\n        }\n    }\n\n    private boolean invokePredicate(MethodHandle predicate, Object value) {\n        try {\n            return (boolean) predicate.invoke(value);\n        } catch (Throwable e) {\n            throw new RuntimeException(\"Could not invoke predicate\", e);\n        }\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    public record Constraint(String formatted, Predicate predicate) {\n        public boolean test(Object value) {\n            return this.predicate.test(value);\n        }\n    }\n\n    public record SerializationBuilder(Jankson.Builder janksonBuilder, ReflectiveEndecBuilder endecBuilder) {\n        public <T> SerializationBuilder addEndec(Class<T> clazz, Endec<T> endec) {\n            endecBuilder().register(endec, clazz);\n\n            janksonBuilder()\n                    .registerSerializer(clazz, (t, marshaller) -> endec.encodeFully(JanksonSerializer::of, t))\n                    .registerDeserializer(JsonElement.class, clazz, (element, marshaller) -> endec.decodeFully(JanksonDeserializer::of, element));\n\n            return this;\n        }\n    }\n\n    public interface BuilderConsumer {\n        void build(SerializationBuilder builder);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/Option.java",
    "content": "package io.wispforest.owo.config;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.config.annotation.RestartRequired;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.network.FriendlyByteBuf;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Field;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.Consumer;\n\n/**\n * Describes a single option in a config. Instances\n * of this class keep a reference to the field in\n * the model class which stores the value used for serialization.\n * <p>\n * An option may enter the so-called \"detached\" state, which means\n * its value is being overridden by the server. In this state, the option\n * is completely immutable and can only be changed again afterwards\n */\npublic final class Option<T> {\n\n    private final String configName;\n    private final Key key;\n    private final String translationKey;\n\n    private final T defaultValue;\n    private final Observable<T> mirror;\n\n    private final BoundField<T> backingField;\n    private final Class<T> clazz;\n\n    private final ConfigWrapper.@Nullable Constraint constraint;\n    private final @Nullable Endec<T> endec;\n    private final SyncMode syncMode;\n\n    /**\n     * Indicates whether this option is currently being overridden\n     * by the server and should thus never synchronize with its backing\n     * field and behave immutably to the client\n     */\n    private boolean detached = false;\n\n    /**\n     * @param configName   The name of the config this option is contained in\n     * @param key          The key of this option\n     * @param defaultValue The default value of this option\n     * @param mirror       A mirror of the value of this option, used for\n     *                     emitting events when it changes as well as correcting\n     *                     invalid values after deserialization\n     * @param backingField The backing field in the config model class\n     *                     which this option describes\n     * @param constraint   The constraint placed on the value of this option,\n     *                     or {@code null} if the option is unconstrained\n     */\n    @SuppressWarnings(\"unchecked\")\n    public Option(String configName,\n                  Key key,\n                  T defaultValue,\n                  Observable<T> mirror,\n                  BoundField<T> backingField,\n                  @Nullable ConfigWrapper.Constraint constraint,\n                  SyncMode syncMode,\n                  ReflectiveEndecBuilder builder\n    ) {\n        this.configName = configName;\n        this.key = key;\n        this.translationKey = \"text.config.\" + this.configName + \".option.\" + this.key.asString();\n\n        this.defaultValue = defaultValue;\n        this.mirror = mirror;\n\n        this.backingField = backingField;\n        this.clazz = (Class<T>) backingField.field().getType();\n\n        this.constraint = constraint;\n        this.syncMode = syncMode;\n        this.endec = syncMode.isNone() ? null : (Endec<T>) builder.get(this.backingField.field.getGenericType());\n    }\n\n    /**\n     * Update the current value of this option,\n     * or do nothing if the given value is invalid\n     *\n     * @param value The new value of the option\n     */\n    public void set(T value) {\n        if (this.detached) return;\n\n        if (!this.verifyConstraint(value)) return;\n\n        this.backingField.setValue(value);\n        this.mirror.set(value);\n    }\n\n    /**\n     * @return The current value of this option\n     */\n    public T value() {\n        return this.mirror.get();\n    }\n\n    /**\n     * @return The class of this option's value\n     */\n    public Class<T> clazz() {\n        return this.clazz;\n    }\n\n    /**\n     * Synchronize the value stored in the backing field\n     * and this option's mirror - used for either correcting an\n     * invalid value after updating the field or updating the mirror\n     */\n    public void synchronizeWithBackingField() {\n        if (this.detached) return;\n\n        final var fieldValue = (T) this.backingField.getValue();\n        if (verifyConstraint(fieldValue)) {\n            this.mirror.set(fieldValue);\n        } else {\n            this.backingField.setValue(this.mirror.get());\n        }\n    }\n\n    /**\n     * Check whether the given value passes the constraint\n     * of this option and emit a warning if it does not\n     *\n     * @param value The value to test\n     * @return {@code true} if either the given value\n     * passes the constraint put on this option or this\n     * option is unconstrained\n     */\n    public boolean verifyConstraint(T value) {\n        if (this.constraint == null) return true;\n\n        final var matched = this.constraint.test(value);\n        if (!matched) {\n            Owo.LOGGER.warn(\n                    \"Option {} in config '{}' could not be updated, as the given value '{}' does not match its constraint: {}\",\n                    this.key, this.configName, value, this.constraint.formatted()\n            );\n        }\n\n        return matched;\n    }\n\n    /**\n     * Add an observer function to be run every time\n     * the value of this option changes\n     */\n    public void observe(Consumer<T> observer) {\n        this.mirror.observe(observer);\n    }\n\n    /**\n     * Write the current value of this option into the given buffer\n     *\n     * @param buf The packet buffer to write to\n     */\n    void write(FriendlyByteBuf buf) {\n        buf.write(this.endec, this.value());\n    }\n\n    /**\n     * Read a new value of this option from the given buffer\n     * and enter a detached state\n     *\n     * @param buf The packet buffer to read from\n     * @return {@code null} if this option was successfully detached,\n     * the server's value otherwise\n     */\n    T read(FriendlyByteBuf buf) {\n        final var newValue = buf.read(this.endec);\n\n        if (!Objects.equals(newValue, this.value()) && this.backingField.hasAnnotation(RestartRequired.class)) {\n            return newValue;\n        }\n\n        this.mirror.set(newValue);\n        this.detached = true;\n\n        return null;\n    }\n\n    /**\n     * @return The serializer for this option's value\n     */\n    Endec<T> endec() {\n        return this.endec;\n    }\n\n    /**\n     * Reset this option's attached state and synchronize\n     * it with the backing field again\n     */\n    void reattach() {\n        if (!this.detached) return;\n\n        this.detached = false;\n        this.synchronizeWithBackingField();\n    }\n\n    // -------------\n\n    /**\n     * @return The translation key of this option\n     */\n    public String translationKey() {\n        return this.translationKey;\n    }\n\n    /**\n     * @return The name of the config this option is contained in\n     */\n    public String configName() {\n        return configName;\n    }\n\n    /**\n     * @return The key of this option\n     */\n    public Key key() {\n        return key;\n    }\n\n    /**\n     * @return The default value of this option\n     */\n    public T defaultValue() {\n        return defaultValue;\n    }\n\n    /**\n     * @return The field which is backing this option,\n     * used for serialization as well as storing the client's\n     * value while the option is detached\n     */\n    public BoundField<T> backingField() {\n        return backingField;\n    }\n\n    /**\n     * @return The constraint placed on the value of this option,\n     * or {@code null} if the option is unconstrained\n     */\n    public ConfigWrapper.@Nullable Constraint constraint() {\n        return constraint;\n    }\n\n    /**\n     * @return {@code true} if this option is currently detached\n     */\n    public boolean detached() {\n        return this.detached;\n    }\n\n    /**\n     * @return The way in which this option\n     * should be synchronized between sever and client\n     */\n    public SyncMode syncMode() {\n        return this.syncMode;\n    }\n\n    @Override\n    public String toString() {\n        return \"Option[\" +\n                \"configName=\" + configName + \", \" +\n                \"key=\" + key + \", \" +\n                \"defaultValue=\" + defaultValue + \", \" +\n                \"constraint=\" + (constraint == null ? null : constraint.formatted())\n                + \"]\";\n    }\n\n    // -------------\n\n    public enum SyncMode {\n        /**\n         * Do not ever send this option over the network\n         */\n        NONE,\n        /**\n         * Only send the client's value to the server,\n         * but not vice-versa\n         */\n        INFORM_SERVER,\n        /**\n         * Send the client's value to the server\n         * <i>and</i> send the server's value back,\n         * overriding the client's value\n         */\n        OVERRIDE_CLIENT;\n\n        public boolean isNone() {\n            return this == NONE;\n        }\n    }\n\n    /**\n     * Describes an option's location inside a\n     * config, generated from its name a potential\n     * parents it is nested in\n     *\n     * @param path The segments of the path making up this key\n     */\n    public record Key(String[] path) {\n\n        public static final Key ROOT = new Key(new String[0]);\n\n        public Key(List<String> path) {\n            this(path.toArray(String[]::new));\n        }\n\n        public Key(String key) {\n            this(key.split(\"\\\\.\"));\n        }\n\n        /**\n         * @return The immediate parent of this key,\n         * or {@link #ROOT} if the parent is the root key\n         */\n        public Key parent() {\n            if (this.path.length <= 1) return ROOT;\n\n            var newPath = new String[this.path.length - 1];\n            System.arraycopy(this.path, 0, newPath, 0, this.path.length - 1);\n            return new Key(newPath);\n        }\n\n        /**\n         * Create the key for a child of this key\n         *\n         * @param childName The name of the child\n         */\n        public Key child(String childName) {\n            var newPath = new String[this.path.length + 1];\n            System.arraycopy(this.path, 0, newPath, 0, this.path.length);\n            newPath[this.path.length] = childName;\n            return new Key(newPath);\n        }\n\n        /**\n         * @return The segments of this key joined with {@code .}\n         */\n        public String asString() {\n            return String.join(\".\", this.path);\n        }\n\n        /**\n         * @return The name of the element this key describes,\n         * without any of its parents\n         */\n        public String name() {\n            if (this.path.length < 1) return \"\";\n            return this.path[this.path.length - 1];\n        }\n\n        /**\n         * @return {@code true} if and only if this\n         * key is reference-equal to {@link #ROOT}\n         */\n        public boolean isRoot() {\n            return this == ROOT;\n        }\n\n        // Records don't play nicely with arrays, thus need to manually\n        // declare all the record autogenerated stuff here\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            Key key = (Key) o;\n            return Arrays.equals(path, key.path);\n        }\n\n        @Override\n        public int hashCode() {\n            return Arrays.hashCode(path);\n        }\n\n        @Override\n        public String toString() {\n            return \"Key{\" + \"path=\" + Arrays.toString(path) + '}';\n        }\n    }\n\n    /**\n     * A simple container which stores both a non-static field\n     * and an instance of the containing class on which to query\n     * values\n     *\n     * @param owner The owner object which holds the value\n     *              the field points to\n     * @param field The field itself\n     * @param <T>   The type of object this field stores\n     */\n    @SuppressWarnings(\"unchecked\")\n    public record BoundField<T>(Object owner, Field field) {\n\n        public boolean hasAnnotation(Class<? extends Annotation> annotationClass) {\n            return field.isAnnotationPresent(annotationClass);\n        }\n\n        public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {\n            return this.field.getAnnotation(annotationClass);\n        }\n\n        public T getValue() {\n            try {\n                return (T) this.field.get(this.owner);\n            } catch (IllegalAccessException e) {\n                throw new RuntimeException(\"Could not access config option field \" + field.getName(), e);\n            }\n        }\n\n        public void setValue(T value) {\n            try {\n                this.field.set(this.owner, value);\n            } catch (IllegalAccessException e) {\n                throw new RuntimeException(\"Could not set config option field \" + field.getName(), e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/OwoConfigCommand.java",
    "content": "package io.wispforest.owo.config;\n\nimport com.mojang.brigadier.CommandDispatcher;\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.arguments.ArgumentType;\nimport com.mojang.brigadier.context.CommandContext;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport com.mojang.brigadier.exceptions.SimpleCommandExceptionType;\nimport com.mojang.brigadier.suggestion.Suggestions;\nimport com.mojang.brigadier.suggestion.SuggestionsBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.config.ui.ConfigScreen;\nimport io.wispforest.owo.config.ui.ConfigScreenProviders;\nimport io.wispforest.owo.ops.TextOps;\nimport net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;\nimport net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.commands.CommandBuildContext;\nimport net.minecraft.commands.SharedSuggestionProvider;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.ArrayList;\nimport java.util.concurrent.CompletableFuture;\n\n@ApiStatus.Internal\npublic class OwoConfigCommand {\n\n    public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandBuildContext access) {\n        dispatcher.register(ClientCommandManager.literal(\"owo-config\")\n                .then(ClientCommandManager.argument(\"config_id\", new ConfigScreenArgumentType())\n                        .executes(context -> {\n                            var screen = context.getArgument(\"config_id\", ConfigScreen.class);\n                            Minecraft.getInstance().schedule(() -> Minecraft.getInstance().setScreen(screen));\n                            return 0;\n                        })));\n    }\n\n    private static class ConfigScreenArgumentType implements ArgumentType<Screen> {\n\n        private static final SimpleCommandExceptionType NO_SUCH_CONFIG_SCREEN = new SimpleCommandExceptionType(\n                TextOps.concat(Owo.PREFIX, Component.literal(\"no config screen with that id\"))\n        );\n\n        @Override\n        public Screen parse(StringReader reader) throws CommandSyntaxException {\n            var provider = ConfigScreenProviders.get(reader.readString());\n            if (provider == null) throw NO_SUCH_CONFIG_SCREEN.create();\n\n            return provider.apply(null);\n        }\n\n        @Override\n        public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {\n            var configNames = new ArrayList<String>();\n            ConfigScreenProviders.forEach((s, screenFunction) -> configNames.add(s));\n            return SharedSuggestionProvider.suggest(configNames, builder);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/Config.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a class to mark is as a config model. This means an\n * implementation of {@link io.wispforest.owo.config.ConfigWrapper}\n * will be generated which can subsequently be used to manage\n * the config data described by the annotated class\n *\n * @see io.wispforest.owo.config.ConfigWrapper\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface Config {\n    /**\n     * @return The name of the wrapper class to generate\n     */\n    String wrapperName();\n\n    /**\n     * @return The name under which to save the config\n     */\n    String name();\n\n    /**\n     * @return {@code true} if all fields should be treated\n     * as if they were annotated with {@link Hook}\n     */\n    boolean defaultHook() default false;\n\n    /**\n     * @return {@code true} if this config should automatically\n     * be saved whenever it is modified\n     */\n    boolean saveOnModification() default true;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/ExcludeFromScreen.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a field to declare that\n * it should be ignored when generating\n * the config screen\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface ExcludeFromScreen {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/Expanded.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Declares that the annotated, collapsible\n * config option (list or nested object) should start\n * expanded when the config screen is opened\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface Expanded {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/Hook.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a field to declare that a method for\n * registering subscribers should be generated\n */\n@Target(ElementType.FIELD)\npublic @interface Hook {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/Modmenu.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a class also annotated with {@link Config}\n * to indicate that a standard owo-config screen should\n * automatically be provided to <a href=\"https://modrinth.com/mod/modmenu\">ModMenu</a>.\n * <p>\n * In case you want more specific control over the generated\n * screen, potentially with a special subclass, you should instead\n * implement {@link com.terraformersmc.modmenu.api.ModMenuApi} like usual\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface Modmenu {\n\n    /**\n     * @return The mod ID for which to register\n     * the config screen factory\n     */\n    String modId();\n\n    /**\n     * @return The ID of the UI model to use for the screen.\n     * You can change this to a model you provide in your\n     * mod's resources to customize the generated screen\n     */\n    String uiModelId() default \"owo:config\";\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/Nest.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a class to declare that instances of it\n * should be treated as a container for nested options\n * within a class annotated with {@link Config} instead of\n * as an option in itself\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface Nest {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/PredicateConstraint.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to fields to define the name of a predicate\n * method to use for verifying values of said field\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface PredicateConstraint {\n    String value();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/RangeConstraint.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to fields with a numeric value to express\n * a range of values which should be accepted\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface RangeConstraint {\n    double min();\n\n    double max();\n\n    /**\n     * @return How many decimals places to show in the config\n     * screen, if this is a floating point option\n     */\n    int decimalPlaces() default 2;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/RegexConstraint.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to fields which can be represented as a {@link CharSequence}\n * to define a regular expressions all values need to match\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface RegexConstraint {\n\n    String value();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/RestartRequired.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a field to indicate\n * that changes made to its value will only\n * apply after a restart of the game\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface RestartRequired {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/SectionHeader.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a field to indicate that\n * the generated screen should prepend\n * a section header to option\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface SectionHeader {\n    /**\n     * @return The name of the section describe by this annotation. Used to\n     * derive a translation key with the pattern {@code text.config.<config name>.section.<name>}\n     */\n    String value();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/Sync.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport io.wispforest.owo.config.Option;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a field to indicate that\n * its value should be synchronized between server\n * and client in some way\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.FIELD, ElementType.TYPE})\npublic @interface Sync {\n    Option.SyncMode value();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/annotation/WithAlpha.java",
    "content": "package io.wispforest.owo.config.annotation;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Applied to a config option of type\n * {@link io.wispforest.owo.ui.core.Color} to indicate\n * that the config screen should expose the alpha\n * component\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface WithAlpha {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/ConfigScreen.java",
    "content": "package io.wispforest.owo.config.ui;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.config.ConfigWrapper;\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.config.annotation.ExcludeFromScreen;\nimport io.wispforest.owo.config.annotation.Expanded;\nimport io.wispforest.owo.config.annotation.RestartRequired;\nimport io.wispforest.owo.config.annotation.SectionHeader;\nimport io.wispforest.owo.config.ui.component.*;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.LabelComponent;\nimport io.wispforest.owo.ui.component.TextBoxComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.CollapsibleContainer;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.container.ScrollContainer;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.UISounds;\nimport io.wispforest.owo.util.NumberReflection;\nimport io.wispforest.owo.util.ReflectionUtils;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.resources.language.I18n;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.FormattedCharSequence;\nimport org.apache.commons.lang3.mutable.MutableBoolean;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.lang.reflect.Field;\nimport java.util.*;\nimport java.util.function.Predicate;\n\n/**\n * A screen which generates components for each option in the\n * provided config. The general structure of the screen is determined\n * by the XML config model it uses - the default one is located at\n * {@code assets/owo/owo_ui/config.xml}. Changing which model is used\n * via {@link #createWithCustomModel(Identifier, ConfigWrapper, Screen)}\n * can often be enough to visually customize the generated screen - should\n * you need custom functionality however, extending this class is usually\n * your best bet\n *\n * @see io.wispforest.owo.config.annotation.Modmenu\n * @see ConfigWrapper\n */\npublic class ConfigScreen extends BaseUIModelScreen<FlowLayout> {\n\n    public static final Identifier DEFAULT_MODEL_ID = Owo.id(\"config\");\n\n    private static final Map<Predicate<Option<?>>, OptionComponentFactory<?>> DEFAULT_FACTORIES = new HashMap<>();\n    /**\n     * A set of extra option factories - add to this if you want to override\n     * some default factories or add extra ones for specific config options\n     * the standard ones don't support\n     */\n    protected final Map<Predicate<Option<?>>, OptionComponentFactory<?>> extraFactories = new HashMap<>();\n\n    protected final Screen parent;\n    protected final ConfigWrapper<?> config;\n    @SuppressWarnings(\"rawtypes\") protected final Map<Option, OptionValueProvider> options = new HashMap<>();\n\n    protected String lastSearchFieldText = \"\";\n    protected @Nullable SearchMatches currentMatches = null;\n    protected int currentMatchIndex = 0;\n\n    protected ConfigScreen(Identifier modelId, ConfigWrapper<?> config, @Nullable Screen parent) {\n        super(FlowLayout.class, DataSource.asset(modelId));\n        this.parent = parent;\n        this.config = config;\n    }\n\n    /**\n     * Create a config screen with the default model ({@code owo:config})\n     *\n     * @param config The config to create a screen for\n     * @param parent The parent screen to return to\n     *               when the created screen is closed\n     */\n    public static ConfigScreen create(ConfigWrapper<?> config, @Nullable Screen parent) {\n        return new ConfigScreen(DEFAULT_MODEL_ID, config, parent);\n    }\n\n    /**\n     * Create a config screen with a custom model\n     * located in your mod's assets\n     *\n     * @param modelId The ID of the model to use\n     * @param config  The config to create a screen for\n     * @param parent  The parent screen to return to\n     *                when the created screen is closed\n     */\n    public static ConfigScreen createWithCustomModel(Identifier modelId, ConfigWrapper<?> config, @Nullable Screen parent) {\n        return new ConfigScreen(modelId, config, parent);\n    }\n\n    @Override\n    @SuppressWarnings({\"ConstantConditions\", \"unchecked\"})\n    protected void build(FlowLayout rootComponent) {\n        this.options.clear();\n\n        rootComponent.childById(LabelComponent.class, \"title\").text(Component.translatable(\"text.config.\" + this.config.name() + \".title\"));\n        if (this.minecraft.level == null) {\n            rootComponent.surface(Surface.optionsBackground());\n        }\n\n        rootComponent.childById(ButtonComponent.class, \"done-button\").onPress(button -> this.onClose());\n        rootComponent.childById(ButtonComponent.class, \"reload-button\").onPress(button -> {\n            this.config.load();\n            this.uiAdapter = null;\n            this.rebuildWidgets();\n\n            // TODO check if any options changed and warn\n        });\n\n        var optionPanel = rootComponent.childById(FlowLayout.class, \"option-panel\");\n        var sections = new LinkedHashMap<UIComponent, Component>();\n\n        var containers = new HashMap<Option.Key, FlowLayout>();\n        containers.put(Option.Key.ROOT, optionPanel);\n\n        rootComponent.childById(TextBoxComponent.class, \"search-field\").<TextBoxComponent>configure(searchField -> {\n            var matchIndicator = rootComponent.childById(LabelComponent.class, \"search-match-indicator\");\n            var optionScroll = rootComponent.childById(ScrollContainer.class, \"option-panel-scroll\");\n\n            var searchHint = I18n.get(\"text.owo.config.search\");\n            searchField.setSuggestion(searchHint);\n            searchField.onChanged().subscribe(s -> {\n                searchField.setSuggestion(s.isEmpty() ? searchHint : \"\");\n                if (!s.equals(this.lastSearchFieldText)) {\n                    searchField.setTextColor(TextBoxComponent.DEFAULT_TEXT_COLOR);\n                    matchIndicator.text(Component.empty());\n                }\n            });\n\n            searchField.keyPress().subscribe((input) -> {\n                if (!input.isConfirmation()) return false;\n\n                var query = searchField.getValue().toLowerCase(Locale.ROOT);\n                if (query.isBlank()) return false;\n\n                if (this.currentMatches != null && this.currentMatches.query.equals(query)) {\n                    if (this.currentMatches.matches().isEmpty()) {\n                        this.currentMatchIndex = -1;\n                    } else {\n                        this.currentMatchIndex = (this.currentMatchIndex + 1) % this.currentMatches.matches.size();\n                    }\n                } else {\n                    var splitQuery = query.split(\" \");\n\n                    this.currentMatchIndex = 0;\n                    this.currentMatches = new SearchMatches(query, this.collectSearchAnchors(optionScroll)\n                        .stream()\n                        .filter(anchor -> Arrays.stream(splitQuery).allMatch(anchor.currentSearchText()::contains))\n                        .toList());\n                }\n\n                if (this.currentMatches.matches.isEmpty()) {\n                    matchIndicator.text(Component.translatable(\"text.owo.config.search.no_matches\"));\n                    searchField.setTextColor(0xEB1D36);\n                } else {\n                    matchIndicator.text(Component.translatable(\"text.owo.config.search.matches\", this.currentMatchIndex + 1, this.currentMatches.matches.size()));\n                    searchField.setTextColor(0x28FFBF);\n\n                    var selectedMatch = this.currentMatches.matches.get(this.currentMatchIndex);\n                    var anchorFrame = selectedMatch.anchorFrame();\n\n                    // we specifically build the path backwards, so we can then iterate\n                    // it root -> key, otherwise we could potentially be manipulating\n                    // unmounted components which is absolutely not desirable\n                    var pathToRoot = new ArrayDeque<Option.Key>();\n                    var key = selectedMatch.key();\n                    while (!key.isRoot()) {\n                        pathToRoot.push(key);\n                        key = key.parent();\n                    }\n\n                    while (!pathToRoot.isEmpty()) {\n                        if (containers.get(pathToRoot.pop()) instanceof CollapsibleContainer collapsible && !collapsible.expanded()) {\n                            collapsible.toggleExpansion();\n                        }\n                    }\n\n                    // in the same vein, the component is mounted after the layout is fully\n                    // restored, as we would otherwise be mounting onto a partially-built subtree\n                    if (anchorFrame instanceof FlowLayout flow) {\n                        flow.child(0, selectedMatch.configure(new SearchHighlighterComponent()));\n                    }\n\n                    if (anchorFrame.y() < optionScroll.y() || anchorFrame.y() + anchorFrame.height() > optionScroll.y() + optionScroll.height()) {\n                        optionScroll.scrollTo(selectedMatch.anchorFrame());\n                    }\n                }\n\n                return true;\n            });\n        });\n\n        this.config.forEachOption(option -> {\n            if (option.backingField().hasAnnotation(ExcludeFromScreen.class)) return;\n\n            var parentKey = option.key().parent();\n            if (!parentKey.isRoot() && this.config.fieldForKey(parentKey).isAnnotationPresent(ExcludeFromScreen.class)) return;\n\n            var factory = this.factoryForOption(option);\n            if (factory == null) {\n                Owo.LOGGER.warn(\"Could not create UI component for config option {}\", option);\n                return;\n            }\n\n            var result = factory.make(this.model, option);\n            this.options.put(option, result.optionProvider());\n\n            var expanded = !parentKey.isRoot() && this.config.fieldForKey(parentKey).isAnnotationPresent(Expanded.class);\n            var container = containers.getOrDefault(\n                parentKey,\n                UIContainers.collapsible(\n                    Sizing.fill(100), Sizing.content(),\n                    Component.translatable(\"text.config.\" + this.config.name() + \".category.\" + parentKey.asString()),\n                    expanded\n                ).<CollapsibleContainer>configure(nestedContainer -> {\n                    final var categoryKey = \"text.config.\" + this.config.name() + \".category.\" + parentKey.asString();\n                    if (I18n.exists(categoryKey + \".tooltip\")) {\n                        nestedContainer.titleLayout().tooltip(Component.translatable(categoryKey + \".tooltip\"));\n                    }\n\n                    nestedContainer.titleLayout().child(new SearchAnchorComponent(\n                        nestedContainer.titleLayout(),\n                        option.key(),\n                        () -> I18n.get(categoryKey)\n                    ).highlightConfigurator(highlight ->\n                        highlight.positioning(Positioning.absolute(-5, -5))\n                            .verticalSizing(Sizing.fixed(19))\n                    ));\n                })\n            );\n\n            if (!containers.containsKey(parentKey) && containers.containsKey(parentKey.parent())) {\n                if (this.config.fieldForKey(parentKey).isAnnotationPresent(SectionHeader.class)) {\n                    this.appendSection(sections, this.config.fieldForKey(parentKey), containers.get(parentKey.parent()));\n                }\n\n                containers.put(parentKey, container);\n                containers.get(parentKey.parent()).child(container);\n            }\n\n            if (option.detached()) {\n                result.baseComponent().tooltip(\n                    this.minecraft.font.split(Component.translatable(\"text.owo.config.managed_by_server\"), Integer.MAX_VALUE)\n                        .stream().map(ClientTooltipComponent::create).toList()\n                );\n            } else {\n                var tooltipText = new ArrayList<FormattedCharSequence>();\n                var tooltipTranslationKey = option.translationKey() + \".tooltip\";\n\n                if (I18n.exists(tooltipTranslationKey)) {\n                    tooltipText.addAll(this.minecraft.font.split(Component.translatable(tooltipTranslationKey), Integer.MAX_VALUE));\n                }\n\n                if (option.backingField().hasAnnotation(RestartRequired.class)) {\n                    tooltipText.add(Component.translatable(\"text.owo.config.applies_after_restart\").getVisualOrderText());\n                }\n\n                if (!tooltipText.isEmpty()) {\n                    result.baseComponent().tooltip(tooltipText.stream().map(ClientTooltipComponent::create).toList());\n                }\n            }\n\n            if (option.backingField().hasAnnotation(SectionHeader.class)) {\n                this.appendSection(sections, option.backingField().field(), container);\n            }\n\n            container.child(result.baseComponent());\n        });\n\n        if (!sections.isEmpty()) {\n            var panelContainer = rootComponent.childById(FlowLayout.class, \"option-panel-container\");\n            var panelScroll = rootComponent.childById(ScrollContainer.class, \"option-panel-scroll\");\n            panelScroll.margins(Insets.right(10));\n\n            var buttonPanel = this.model.expandTemplate(FlowLayout.class, \"section-buttons\", Map.of());\n            sections.forEach((component, text) -> {\n                var hoveredText = text.copy().withStyle(ChatFormatting.YELLOW);\n\n                final var label = UIComponents.label(text);\n                label.cursorStyle(CursorStyle.HAND).margins(Insets.of(2));\n\n                label.mouseEnter().subscribe(() -> label.text(hoveredText));\n                label.mouseLeave().subscribe(() -> label.text(text));\n\n                label.mouseDown().subscribe((click, doubled) -> {\n                    panelScroll.scrollTo(component);\n                    UISounds.playInteractionSound();\n                    return true;\n                });\n\n                buttonPanel.child(label);\n            });\n\n            var closeButton = UIComponents.label(Component.literal(\"<\").withStyle(ChatFormatting.BOLD));\n            closeButton.tooltip(Component.translatable(\"text.owo.config.sections_tooltip\"));\n            closeButton.positioning(Positioning.relative(100, 50)).cursorStyle(CursorStyle.HAND).margins(Insets.right(2));\n\n            panelContainer.child(closeButton);\n            panelContainer.mouseDown().subscribe((click, doubled) -> {\n                if (click.x() < panelContainer.width() - 10) return false;\n\n                if (buttonPanel.horizontalSizing().animation() == null) {\n                    buttonPanel.horizontalSizing().animate(350, Easing.CUBIC, Sizing.content());\n                }\n\n                buttonPanel.horizontalSizing().animation().reverse();\n                closeButton.text(Component.literal(closeButton.text().getString().equals(\">\") ? \"<\" : \">\").withStyle(ChatFormatting.BOLD));\n\n                UISounds.playInteractionSound();\n                return true;\n            });\n\n            rootComponent.childById(FlowLayout.class, \"main-panel\").child(buttonPanel);\n        }\n    }\n\n    protected void appendSection(Map<UIComponent, Component> sections, Field field, FlowLayout container) {\n        var translationKey = \"text.config.\" + this.config.name() + \".section.\"\n            + field.getAnnotation(SectionHeader.class).value();\n\n        final var header = this.model.expandTemplate(FlowLayout.class, \"section-header\", Map.of());\n        header.childById(LabelComponent.class, \"header\").<LabelComponent>configure(label -> {\n            label.text(Component.translatable(translationKey).withStyle(ChatFormatting.YELLOW, ChatFormatting.BOLD));\n            header.child(new SearchAnchorComponent(header, Option.Key.ROOT, () -> label.text().getString()));\n        });\n\n        sections.put(header, Component.translatable(translationKey));\n\n        container.child(header);\n    }\n\n    protected List<SearchAnchorComponent> collectSearchAnchors(ParentUIComponent root) {\n        var discovered = new ArrayList<SearchAnchorComponent>();\n        var candidates = new ArrayDeque<>(root.children());\n\n        while (!candidates.isEmpty()) {\n            var candidate = candidates.poll();\n            if (candidate instanceof CollapsibleContainer collapsible) {\n                candidates.addAll(collapsible.children());\n                if (!collapsible.expanded()) candidates.addAll(collapsible.collapsibleChildren());\n            } else if (candidate instanceof ParentUIComponent parentComponent) {\n                candidates.addAll(parentComponent.children());\n            } else if (candidate instanceof SearchAnchorComponent anchor) {\n                discovered.add(anchor);\n            }\n        }\n\n        return discovered;\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (input.key() == GLFW.GLFW_KEY_F && input.hasControlDown()) {\n            this.uiAdapter.rootComponent.focusHandler().focus(\n                this.uiAdapter.rootComponent.childById(UIComponent.class, \"search-field\"),\n                UIComponent.FocusSource.MOUSE_CLICK\n            );\n            return true;\n        } else {\n            return super.keyPressed(input);\n        }\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void onClose() {\n        var shouldRestart = new MutableBoolean();\n        this.options.forEach((option, component) -> {\n            if (!option.backingField().hasAnnotation(RestartRequired.class)) return;\n            if (Objects.equals(option.value(), component.parsedValue())) return;\n\n            shouldRestart.setTrue();\n        });\n\n        this.minecraft.setScreen(shouldRestart.booleanValue() ? new RestartRequiredScreen(this.parent) : this.parent);\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void removed() {\n        this.options.forEach((option, component) -> {\n            if (!component.isValid()) return;\n            option.set(component.parsedValue());\n        });\n        super.removed();\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    protected @Nullable OptionComponentFactory factoryForOption(Option<?> option) {\n        for (var predicate : this.extraFactories.keySet()) {\n            if (!predicate.test(option)) continue;\n            return this.extraFactories.get(predicate);\n        }\n\n        for (var predicate : DEFAULT_FACTORIES.keySet()) {\n            if (!predicate.test(option)) continue;\n            return DEFAULT_FACTORIES.get(predicate);\n        }\n\n        return null;\n    }\n\n    static {\n        DEFAULT_FACTORIES.put(option -> NumberReflection.isNumberType(option.clazz()), OptionComponentFactory.NUMBER);\n        DEFAULT_FACTORIES.put(option -> option.clazz() == String.class, OptionComponentFactory.STRING);\n        DEFAULT_FACTORIES.put(option -> option.clazz() == Boolean.class || option.clazz() == boolean.class, OptionComponentFactory.BOOLEAN);\n        DEFAULT_FACTORIES.put(option -> option.clazz() == Identifier.class, OptionComponentFactory.IDENTIFIER);\n        DEFAULT_FACTORIES.put(option -> option.clazz() == Color.class, OptionComponentFactory.COLOR);\n        DEFAULT_FACTORIES.put(option -> isStringOrNumberList(option.backingField().field()), OptionComponentFactory.LIST);\n        DEFAULT_FACTORIES.put(option -> option.clazz().isEnum(), OptionComponentFactory.ENUM);\n\n        UIParsing.registerFactory(\"config-slider\", element -> new ConfigSlider());\n        UIParsing.registerFactory(\"config-toggle-button\", element -> new ConfigToggleButton());\n        UIParsing.registerFactory(\"config-enum-button\", element -> new ConfigEnumButton());\n        UIParsing.registerFactory(\"config-text-box\", element -> new ConfigTextBox());\n    }\n\n    protected record SearchMatches(String query, List<SearchAnchorComponent> matches) {}\n\n    public static class SearchHighlighterComponent extends BaseUIComponent {\n\n        private final Color startColor = Color.ofArgb(0x008d9be0);\n        private final Color endColor = Color.ofArgb(0x4c8d9be0);\n\n        private float age = 0;\n\n        public SearchHighlighterComponent() {\n            this.positioning(Positioning.absolute(0, 0));\n            this.sizing(Sizing.fill(100), Sizing.fill(100));\n        }\n\n        @Override\n        public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n            final var mainColor = startColor.interpolate(endColor, (float) Math.sin(age / 25 * Math.PI)).argb();\n\n            int segmentWidth = (int) (this.width * .3f);\n            int baseX = (int) ((this.x - segmentWidth) + (Easing.CUBIC.apply(this.age / 25)) * (this.width + segmentWidth * 2));\n\n            graphics.drawGradientRect(\n                baseX - segmentWidth, this.y,\n                segmentWidth, this.height,\n                0, mainColor,\n                mainColor, 0\n            );\n            graphics.drawGradientRect(\n                baseX, this.y,\n                segmentWidth, this.height,\n                mainColor, 0,\n                0, mainColor\n            );\n        }\n\n        @Override\n        public void update(float delta, int mouseX, int mouseY) {\n            super.update(delta, mouseX, mouseY);\n            if ((this.age += delta) > 25) {\n                this.parent.queue(() -> this.parent.removeChild(this));\n            }\n        }\n    }\n\n    private static boolean isStringOrNumberList(Field field) {\n        if (field.getType() != List.class) return false;\n\n        var listType = ReflectionUtils.getTypeArgument(field.getGenericType(), 0);\n        if (listType == null) return false;\n\n        return String.class == listType || NumberReflection.isNumberType(listType);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/ConfigScreenProviders.java",
    "content": "package io.wispforest.owo.config.ui;\n\nimport net.minecraft.client.gui.screens.Screen;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\n\npublic class ConfigScreenProviders {\n\n    private static final Map<String, Function<Screen, ? extends Screen>> PROVIDERS = new HashMap<>();\n    private static final Map<String, Function<Screen, ? extends ConfigScreen>> OWO_SCREEN_PROVIDERS = new HashMap<>();\n\n    /**\n     * Register the given config screen provider. This is primarily\n     * used for making a config screen available in ModMenu and to the\n     * {@code /owo-config} command, although other places my use it as well\n     *\n     * @param modId    The mod id for which to supply a config screen\n     * @param supplier The supplier to register - this gets the parent screen\n     *                 as argument\n     * @throws IllegalArgumentException If a config screen provider is\n     *                                  already registered for the given mod id\n     */\n    public static <S extends Screen> void register(String modId, Function<Screen, S> supplier) {\n        if (PROVIDERS.put(modId, supplier) != null) {\n            throw new IllegalArgumentException(\"Tried to register config screen provider for mod id \" + modId + \" twice\");\n        }\n    }\n\n    /**\n     * Get the config screen provider associated with\n     * the given mod id\n     *\n     * @return The associated config screen provider, or {@code null} if\n     * none is registered\n     */\n    public static @Nullable Function<Screen, ? extends Screen> get(String modId) {\n        return PROVIDERS.get(modId);\n    }\n\n    public static void forEach(BiConsumer<String, Function<Screen, ? extends Screen>> action) {\n        PROVIDERS.forEach(action);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/OptionComponentFactory.java",
    "content": "package io.wispforest.owo.config.ui;\n\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.config.annotation.RangeConstraint;\nimport io.wispforest.owo.config.annotation.WithAlpha;\nimport io.wispforest.owo.config.ui.component.ListOptionContainer;\nimport io.wispforest.owo.config.ui.component.OptionValueProvider;\nimport io.wispforest.owo.ui.component.BoxComponent;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.ColorPickerComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.util.NumberReflection;\nimport net.minecraft.resources.Identifier;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\n/**\n * A function which creates an instance of {@link OptionValueProvider}\n * fitting for the given config option. Whatever component is created\n * should accurately reflect if the option is currently detached\n * and thus immutable - ideally it is non-interactable\n *\n * @param <T> The type of option for which this factory can create components\n */\npublic interface OptionComponentFactory<T> {\n\n    OptionComponentFactory<? extends Number> NUMBER = (model, option) -> {\n        var field = option.backingField().field();\n\n        if (field.isAnnotationPresent(RangeConstraint.class)) {\n            return OptionComponents.createRangeControls(\n                    model, option,\n                    NumberReflection.isFloatingPointType(field.getType())\n                            ? field.getAnnotation(RangeConstraint.class).decimalPlaces()\n                            : 0\n            );\n        } else {\n            return OptionComponents.createTextBox(model, option, configTextBox -> {\n                configTextBox.configureForNumber(option.clazz());\n            });\n        }\n    };\n\n    OptionComponentFactory<? extends CharSequence> STRING = (model, option) -> {\n        return OptionComponents.createTextBox(model, option, configTextBox -> {\n            if (option.constraint() != null) {\n                configTextBox.applyPredicate(option.constraint()::test);\n            }\n        });\n    };\n\n    OptionComponentFactory<Identifier> IDENTIFIER = (model, option) -> {\n        return OptionComponents.createTextBox(model, option, configTextBox -> {\n            configTextBox.inputPredicate(s -> s.matches(\"[a-z0-9_.:\\\\-]*\"));\n            configTextBox.applyPredicate(s -> Identifier.tryParse(s) != null);\n            configTextBox.valueParser(Identifier::parse);\n        });\n    };\n\n    @SuppressWarnings(\"DataFlowIssue\")\n    OptionComponentFactory<Color> COLOR = (model, option) -> {\n        boolean withAlpha = option.backingField().hasAnnotation(WithAlpha.class);\n\n        final var result = OptionComponents.createTextBox(model, option, color -> color.asHexString(withAlpha), configTextBox -> {\n            configTextBox.inputPredicate(withAlpha ? s -> s.matches(\"#[a-zA-Z\\\\d]{0,8}\") : s -> s.matches(\"#[a-zA-Z\\\\d]{0,6}\"));\n            configTextBox.applyPredicate(withAlpha ? s -> s.matches(\"#[a-zA-Z\\\\d]{8}\") : s -> s.matches(\"#[a-zA-Z\\\\d]{6}\"));\n            configTextBox.valueParser(withAlpha\n                    ? s -> Color.ofArgb(Integer.parseUnsignedInt(s.substring(1), 16))\n                    : s -> Color.ofRgb(Integer.parseUnsignedInt(s.substring(1), 16))\n            );\n        });\n\n        result.baseComponent.childById(FlowLayout.class, \"controls-flow\").<FlowLayout>configure(controls -> {\n            Supplier<Color> valueGetter = () -> result.optionProvider.isValid()\n                    ? (Color) result.optionProvider.parsedValue()\n                    : Color.BLACK;\n\n            var box = UIComponents.box(Sizing.fixed(15), Sizing.fixed(15)).color(valueGetter.get()).fill(true);\n            box.margins(Insets.right(5)).cursorStyle(CursorStyle.HAND);\n            controls.child(0, box);\n\n            result.optionProvider.onChanged().subscribe(value -> box.color(valueGetter.get()));\n\n            box.mouseDown().subscribe((click, doubled) -> {\n                ((FlowLayout) box.root()).child(UIContainers.overlay(\n                        model.expandTemplate(\n                                FlowLayout.class,\n                                \"color-picker-panel\",\n                                Map.of(\"color\", valueGetter.get().asHexString(withAlpha), \"with-alpha\", String.valueOf(withAlpha))\n                        ).<FlowLayout>configure(flowLayout -> {\n                            var picker = flowLayout.childById(ColorPickerComponent.class, \"color-picker\");\n                            var previewBox = flowLayout.childById(BoxComponent.class, \"current-color\");\n\n                            picker.onChanged().subscribe(previewBox::color);\n\n                            flowLayout.childById(ButtonComponent.class, \"confirm-button\").onPress(confirmButton -> {\n                                result.optionProvider.text(picker.selectedColor().asHexString(withAlpha));\n                                flowLayout.parent().remove();\n                            });\n\n                            flowLayout.childById(ButtonComponent.class, \"cancel-button\").onPress(cancelButton -> {\n                                flowLayout.parent().remove();\n                            });\n                        })\n                ));\n\n                return true;\n            });\n        });\n\n        return result;\n    };\n\n    OptionComponentFactory<Boolean> BOOLEAN = OptionComponents::createToggleButton;\n\n    OptionComponentFactory<? extends Enum<?>> ENUM = OptionComponents::createEnumButton;\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    OptionComponentFactory<List<?>> LIST = (model, option) -> {\n        var layout = new ListOptionContainer(option);\n        return new Result(layout, layout);\n    };\n\n    /**\n     * Create a new component fitting for, and bound to,\n     * the given config option\n     *\n     * @param model  The UI model of the enclosing screen, used\n     *               for expanding templates\n     * @param option The option for which to create a component\n     * @return The option component as well as a potential wrapping\n     * component, this simply be the option component itself\n     */\n    Result<?, ?> make(UIModel model, Option<T> option);\n\n    record Result<B extends UIComponent, P extends OptionValueProvider>(B baseComponent, P optionProvider) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/OptionComponents.java",
    "content": "package io.wispforest.owo.config.ui;\n\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.config.annotation.RangeConstraint;\nimport io.wispforest.owo.config.ui.component.*;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.LabelComponent;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.Positioning;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport net.minecraft.network.chat.Component;\nimport org.apache.commons.lang3.mutable.MutableBoolean;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n@SuppressWarnings(\"ConstantConditions\")\npublic class OptionComponents {\n\n    public static OptionComponentFactory.Result<FlowLayout, ConfigTextBox> createTextBox(UIModel model, Option<?> option, Consumer<ConfigTextBox> processor) {\n        return createTextBox(model, option, Object::toString, processor);\n    }\n\n    public static <T> OptionComponentFactory.Result<FlowLayout, ConfigTextBox> createTextBox(UIModel model, Option<T> option, Function<T, String> toStringFunction, Consumer<ConfigTextBox> processor) {\n        var optionComponent = model.expandTemplate(FlowLayout.class,\n                \"text-box-config-option\",\n                packParameters(option.translationKey(), toStringFunction.apply(option.value()))\n        );\n\n        var valueBox = optionComponent.childById(ConfigTextBox.class, \"value-box\");\n        var resetButton = optionComponent.childById(ButtonComponent.class, \"reset-button\");\n\n        if (option.detached()) {\n            resetButton.active = false;\n            valueBox.setEditable(false);\n        } else {\n            resetButton.active = !valueBox.getValue().equals(toStringFunction.apply(option.defaultValue()));\n            resetButton.onPress(button -> {\n                valueBox.setValue(toStringFunction.apply(option.defaultValue()));\n                button.active = false;\n            });\n\n            valueBox.onChanged().subscribe(s -> resetButton.active = !s.equals(toStringFunction.apply(option.defaultValue())));\n        }\n\n        processor.accept(valueBox);\n\n        optionComponent.child(new SearchAnchorComponent(\n                optionComponent,\n                option.key(),\n                () -> optionComponent.childById(LabelComponent.class, \"option-name\").text().getString(),\n                valueBox::getValue\n        ));\n\n        return new OptionComponentFactory.Result<>(optionComponent, valueBox);\n    }\n\n    public static OptionComponentFactory.Result<FlowLayout, OptionValueProvider> createRangeControls(UIModel model, Option<? extends Number> option, int decimalPlaces) {\n        boolean withDecimals = decimalPlaces > 0;\n\n        // ------------\n        // Slider setup\n        // ------------\n\n        var value = option.value();\n        var optionComponent = model.expandTemplate(FlowLayout.class,\n                \"range-config-option\",\n                packParameters(option.translationKey(), value.toString())\n        );\n\n        var constraint = option.backingField().field().getAnnotation(RangeConstraint.class);\n        double min = constraint.min(), max = constraint.max();\n\n        var sliderInput = optionComponent.childById(ConfigSlider.class, \"value-slider\");\n        sliderInput.min(min).max(max).decimalPlaces(decimalPlaces).snap(!withDecimals).setFromDiscreteValue(value.doubleValue());\n        sliderInput.valueType(option.clazz());\n\n        var resetButton = optionComponent.childById(ButtonComponent.class, \"reset-button\");\n\n        if (option.detached()) {\n            resetButton.active = false;\n            sliderInput.active = false;\n        } else {\n            resetButton.active = (withDecimals ? value.doubleValue() : Math.round(value.doubleValue())) != option.defaultValue().doubleValue();\n            resetButton.onPress(button -> {\n                sliderInput.setFromDiscreteValue(option.defaultValue().doubleValue());\n                button.active = false;\n            });\n\n            sliderInput.onChanged().subscribe(newValue -> {\n                resetButton.active = (withDecimals ? newValue : Math.round(newValue)) != option.defaultValue().doubleValue();\n            });\n        }\n\n        // ------------------------------------\n        // Component handles and text box setup\n        // ------------------------------------\n\n        var sliderControls = optionComponent.childById(FlowLayout.class, \"slider-controls\");\n        var textControls = createTextBox(model, option, configTextBox -> {\n            configTextBox.configureForNumber(option.clazz());\n\n            var predicate = configTextBox.applyPredicate();\n            configTextBox.applyPredicate(predicate.and(s -> {\n                final var parsed = Double.parseDouble(s);\n                return parsed >= min && parsed <= max;\n            }));\n        }).baseComponent().childById(FlowLayout.class, \"controls-flow\").positioning(Positioning.layout());\n        var textInput = textControls.childById(ConfigTextBox.class, \"value-box\");\n\n        // ------------\n        // Toggle setup\n        // ------------\n\n        var controlsLayout = optionComponent.childById(FlowLayout.class, \"controls-flow\");\n        var toggleButton = optionComponent.childById(ButtonComponent.class, \"toggle-button\");\n\n        var textMode = new MutableBoolean(false);\n        toggleButton.onPress(button -> {\n            textMode.setValue(textMode.isFalse());\n\n            if (textMode.isTrue()) {\n                sliderControls.remove();\n                textInput.text(sliderInput.decimalPlaces() == 0 ? String.valueOf((int) sliderInput.discreteValue()) : String.valueOf(sliderInput.discreteValue()));\n\n                controlsLayout.child(textControls);\n            } else {\n                textControls.remove();\n                sliderInput.setFromDiscreteValue(((Number) textInput.parsedValue()).doubleValue());\n\n                controlsLayout.child(sliderControls);\n            }\n\n            button.tooltip(textMode.isTrue()\n                    ? Component.translatable(\"text.owo.config.button.range.edit_with_slider\")\n                    : Component.translatable(\"text.owo.config.button.range.edit_as_text\")\n            );\n        });\n\n        optionComponent.child(new SearchAnchorComponent(\n                optionComponent,\n                option.key(),\n                () -> optionComponent.childById(LabelComponent.class, \"option-name\").text().getString(),\n                () -> textMode.isTrue() ? textInput.getValue() : sliderInput.getMessage().getString()\n        ));\n\n        return new OptionComponentFactory.Result<>(optionComponent, new OptionValueProvider() {\n            @Override\n            public boolean isValid() {\n                return textMode.isTrue()\n                        ? textInput.isValid()\n                        : sliderInput.isValid();\n            }\n\n            @Override\n            public Object parsedValue() {\n                return textMode.isTrue()\n                        ? textInput.parsedValue()\n                        : sliderInput.parsedValue();\n            }\n        });\n    }\n\n    public static OptionComponentFactory.Result<FlowLayout, ConfigToggleButton> createToggleButton(UIModel model, Option<Boolean> option) {\n        var optionComponent = model.expandTemplate(FlowLayout.class,\n                \"boolean-toggle-config-option\",\n                packParameters(option.translationKey(), option.value().toString())\n        );\n\n        var toggleButton = optionComponent.childById(ConfigToggleButton.class, \"toggle-button\");\n        var resetButton = optionComponent.childById(ButtonComponent.class, \"reset-button\");\n\n        toggleButton.enabled(option.value());\n\n        if (option.detached()) {\n            resetButton.active = false;\n            toggleButton.active = false;\n        } else {\n            resetButton.active = option.value() != option.defaultValue();\n            resetButton.onPress(button -> {\n                toggleButton.enabled(option.defaultValue());\n                button.active = false;\n            });\n\n            toggleButton.onPress(button -> resetButton.active = toggleButton.parsedValue() != option.defaultValue());\n        }\n\n        optionComponent.child(new SearchAnchorComponent(\n                optionComponent,\n                option.key(),\n                () -> optionComponent.childById(LabelComponent.class, \"option-name\").text().getString(),\n                () -> toggleButton.getMessage().getString()\n        ));\n\n        return new OptionComponentFactory.Result<>(optionComponent, toggleButton);\n    }\n\n    public static OptionComponentFactory.Result<FlowLayout, ConfigEnumButton> createEnumButton(UIModel model, Option<? extends Enum<?>> option) {\n        var optionComponent = model.expandTemplate(FlowLayout.class,\n                \"enum-config-option\",\n                packParameters(option.translationKey(), option.value().toString())\n        );\n\n        var enumButton = optionComponent.childById(ConfigEnumButton.class, \"enum-button\");\n        var resetButton = optionComponent.childById(ButtonComponent.class, \"reset-button\");\n\n        enumButton.init(option, option.value().ordinal());\n\n        if (option.detached()) {\n            resetButton.active = false;\n            enumButton.active = false;\n        } else {\n            resetButton.active = option.value() != option.defaultValue();\n            resetButton.onPress(button -> {\n                enumButton.select(option.defaultValue().ordinal());\n                button.active = false;\n            });\n\n            enumButton.onPress(button -> resetButton.active = enumButton.parsedValue() != option.defaultValue());\n        }\n\n        optionComponent.child(new SearchAnchorComponent(\n                optionComponent,\n                option.key(),\n                () -> optionComponent.childById(LabelComponent.class, \"option-name\").text().getString(),\n                () -> enumButton.getMessage().getString()\n        ));\n\n        return new OptionComponentFactory.Result<>(optionComponent, enumButton);\n    }\n\n    public static Map<String, String> packParameters(String name, String value) {\n        return Map.of(\n                \"config-option-name\", name,\n                \"config-option-value\", value\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/RestartRequiredScreen.java",
    "content": "package io.wispforest.owo.config.ui;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.Surface;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.Screen;\nimport org.jetbrains.annotations.ApiStatus;\n\n@ApiStatus.Internal\npublic class RestartRequiredScreen extends BaseUIModelScreen<FlowLayout> {\n\n    protected final Screen parent;\n\n    public RestartRequiredScreen(Screen parent) {\n        super(FlowLayout.class, DataSource.asset(Owo.id(\"restart_required\")));\n        this.parent = parent;\n    }\n\n    @Override\n    public void onClose() {\n        this.minecraft.setScreen(parent);\n    }\n\n    @Override\n    @SuppressWarnings(\"ConstantConditions\")\n    protected void build(FlowLayout rootComponent) {\n        if (this.minecraft.level == null) {\n            rootComponent.surface(Surface.optionsBackground());\n        }\n\n        rootComponent.childById(ButtonComponent.class, \"exit-button\")\n                .onPress(button -> Minecraft.getInstance().stop());\n\n        rootComponent.childById(ButtonComponent.class, \"ignore-button\")\n                .onPress(button -> this.onClose());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/component/ConfigEnumButton.java",
    "content": "package io.wispforest.owo.config.ui.component;\n\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.core.Sizing;\nimport net.minecraft.client.input.InputWithModifiers;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.input.MouseButtonInfo;\nimport net.minecraft.client.resources.language.I18n;\nimport net.minecraft.network.chat.Component;\nimport org.apache.commons.lang3.StringUtils;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.Locale;\n\n@ApiStatus.Internal\npublic class ConfigEnumButton extends ButtonComponent implements OptionValueProvider {\n\n    @Nullable protected Option<? extends Enum<?>> backingOption = null;\n    @Nullable protected Enum<?>[] backingValues = null;\n    protected int selectedIndex = 0;\n\n    protected boolean wasRightClicked = false;\n\n    public ConfigEnumButton() {\n        super(Component.empty(), button -> {});\n        this.verticalSizing(Sizing.fixed(20));\n        this.updateMessage();\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        this.wasRightClicked = click.button() == GLFW.GLFW_MOUSE_BUTTON_RIGHT;\n        return super.onMouseDown(click, doubled);\n    }\n\n    @Override\n    public void onPress(InputWithModifiers input) {\n        if (this.wasRightClicked || input.hasShiftDown()) {\n            this.selectedIndex--;\n            if (this.selectedIndex < 0) this.selectedIndex += this.backingValues.length;\n        } else {\n            this.selectedIndex++;\n            if (this.selectedIndex > this.backingValues.length - 1) this.selectedIndex -= this.backingValues.length;\n        }\n\n        this.updateMessage();\n\n        super.onPress(input);\n    }\n\n    @Override\n    protected boolean isValidClickButton(MouseButtonInfo input) {\n        return input.button() == GLFW.GLFW_MOUSE_BUTTON_RIGHT || super.isValidClickButton(input);\n    }\n\n    protected void updateMessage() {\n        if (this.backingOption == null) return;\n\n        var enumName = StringUtils.uncapitalize(this.backingValues.getClass().componentType().getSimpleName());\n        var valueName = this.backingValues[this.selectedIndex].name().toLowerCase(Locale.ROOT);\n\n        var optionValueKey = this.backingOption.translationKey() + \".value.\" + valueName;\n\n        this.setMessage(I18n.exists(optionValueKey)\n                ? Component.translatable(optionValueKey)\n                : Component.translatable(\"text.config.\" + this.backingOption.configName() + \".enum.\" + enumName + \".\" + valueName)\n        );\n    }\n\n    public ConfigEnumButton init(Option<? extends Enum<?>> option, int selectedIndex) {\n        this.backingOption = option;\n        this.backingValues = (Enum<?>[]) option.backingField().field().getType().getEnumConstants();\n        this.selectedIndex = selectedIndex;\n\n        this.updateMessage();\n\n        return this;\n    }\n\n    public ConfigEnumButton select(int index) {\n        this.selectedIndex = index;\n        this.updateMessage();\n\n        return this;\n    }\n\n    @Override\n    public boolean isValid() {\n        return true;\n    }\n\n    @Override\n    public Object parsedValue() {\n        return this.backingValues[this.selectedIndex];\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/component/ConfigSlider.java",
    "content": "package io.wispforest.owo.config.ui.component;\n\nimport io.wispforest.owo.ui.component.DiscreteSliderComponent;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.util.NumberReflection;\nimport org.jetbrains.annotations.ApiStatus;\n\n@ApiStatus.Internal\npublic class ConfigSlider extends DiscreteSliderComponent implements OptionValueProvider {\n\n    protected Class<? extends Number> valueType;\n\n    public ConfigSlider() {\n        super(Sizing.content(), 0, 1);\n    }\n\n    public ConfigSlider valueType(Class<? extends Number> valueType) {\n        this.valueType = valueType;\n        return this;\n    }\n\n    public ConfigSlider min(double min) {\n        this.min = min;\n        return this;\n    }\n\n    public ConfigSlider max(double max) {\n        this.max = max;\n        return this;\n    }\n\n    @Override\n    public boolean isValid() {\n        return true;\n    }\n\n    @Override\n    public Object parsedValue() {\n        double value = this.min + this.value * (this.max - this.min);\n        if (!NumberReflection.isFloatingPointType(this.valueType)) {\n            value = Math.round(value);\n        }\n\n        return NumberReflection.convert(value, this.valueType);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/component/ConfigTextBox.java",
    "content": "package io.wispforest.owo.config.ui.component;\n\nimport io.wispforest.owo.ui.component.TextBoxComponent;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.util.NumberReflection;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\n\n@ApiStatus.Internal\n@SuppressWarnings(\"UnusedReturnValue\")\npublic class ConfigTextBox extends TextBoxComponent implements OptionValueProvider {\n\n    protected int invalidColor = 0xFFEB1D36, validColor = 0xFF28FFBF;\n    protected Function<String, Object> valueParser = s -> s;\n    protected Predicate<String> inputPredicate = s -> true, applyPredicate = s -> true;\n\n    public ConfigTextBox() {\n        super(Sizing.fixed(0));\n        this.setMaxLength(Integer.MAX_VALUE);\n\n        this.textValue.observe(s -> {\n            this.setTextColor(this.applyPredicate.test(s) ? this.validColor : this.invalidColor);\n        });\n    }\n\n    public ConfigTextBox configureForNumber(Class<? extends Number> fieldType) {\n        final boolean floatingPoint = NumberReflection.isFloatingPointType(fieldType);\n        final double min = NumberReflection.minValue(fieldType).doubleValue(), max = NumberReflection.maxValue(fieldType).doubleValue();\n\n        this.valueParser = s -> {\n            try {\n                return NumberReflection.convert(floatingPoint ? Double.parseDouble(s) : Long.parseLong(s), fieldType);\n            } catch (NumberFormatException nfe) {\n                return NumberReflection.convert(0L, fieldType);\n            }\n        };\n\n        this.inputPredicate(floatingPoint ? s -> s.matches(\"-?\\\\d*\\\\.?\\\\d*\") : s -> s.matches(\"-?\\\\d*\"));\n        this.applyPredicate(s -> {\n            try {\n                var value = Double.parseDouble(s);\n                return value >= min && value <= max;\n            } catch (NumberFormatException nfe) {\n                return false;\n            }\n        });\n\n        return this;\n    }\n\n    @Override\n    public boolean isValid() {\n        return this.applyPredicate.test(this.getValue());\n    }\n\n    @Override\n    public Object parsedValue() {\n        return this.valueParser.apply(this.getValue());\n    }\n\n    public ConfigTextBox inputPredicate(Predicate<String> inputPredicate) {\n        this.inputPredicate = inputPredicate;\n        this.setFilter(this.inputPredicate);\n        return this;\n    }\n\n    public Predicate<String> inputPredicate() {\n        return inputPredicate;\n    }\n\n    public ConfigTextBox applyPredicate(Predicate<String> applyPredicate) {\n        this.applyPredicate = applyPredicate;\n        return this;\n    }\n\n    public Predicate<String> applyPredicate() {\n        return applyPredicate;\n    }\n\n    public ConfigTextBox invalidColor(int invalidColor) {\n        this.invalidColor = invalidColor;\n        return this;\n    }\n\n    public int invalidColor() {\n        return invalidColor;\n    }\n\n    public ConfigTextBox validColor(int validColor) {\n        this.validColor = validColor;\n        return this;\n    }\n\n    public int validColor() {\n        return validColor;\n    }\n\n    public Function<String, Object> valueParser() {\n        return this.valueParser;\n    }\n\n    public ConfigTextBox valueParser(Function<String, Object> valueParser) {\n        this.valueParser = valueParser;\n        return this;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"invalid-color\", Color::parseAndPack, this::invalidColor);\n        UIParsing.apply(children, \"valid-color\", Color::parseAndPack, this::validColor);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/component/ConfigToggleButton.java",
    "content": "package io.wispforest.owo.config.ui.component;\n\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.core.Sizing;\nimport net.minecraft.client.input.InputWithModifiers;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.ApiStatus;\n\n@ApiStatus.Internal\npublic class ConfigToggleButton extends ButtonComponent implements OptionValueProvider {\n\n    protected static final Component ENABLED_MESSAGE = Component.translatable(\"text.owo.config.boolean_toggle.enabled\");\n    protected static final Component DISABLED_MESSAGE = Component.translatable(\"text.owo.config.boolean_toggle.disabled\");\n\n    protected boolean enabled = false;\n\n    public ConfigToggleButton() {\n        super(Component.empty(), button -> {});\n        this.verticalSizing(Sizing.fixed(20));\n        this.updateMessage();\n    }\n\n    @Override\n    public void onPress(InputWithModifiers input) {\n        this.enabled = !this.enabled;\n        this.updateMessage();\n        super.onPress(input);\n    }\n\n    protected void updateMessage() {\n        this.setMessage(this.enabled ? ENABLED_MESSAGE : DISABLED_MESSAGE);\n    }\n\n    public ConfigToggleButton enabled(boolean enabled) {\n        this.enabled = enabled;\n        this.updateMessage();\n        return this;\n    }\n\n    @Override\n    public boolean isValid() {\n        return true;\n    }\n\n    @Override\n    public Object parsedValue() {\n        return this.enabled;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/component/ListOptionContainer.java",
    "content": "package io.wispforest.owo.config.ui.component;\n\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.config.annotation.Expanded;\nimport io.wispforest.owo.ops.TextOps;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.component.LabelComponent;\nimport io.wispforest.owo.ui.container.CollapsibleContainer;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.util.UISounds;\nimport io.wispforest.owo.util.NumberReflection;\nimport io.wispforest.owo.util.ReflectionUtils;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.resources.language.I18n;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\n\n@ApiStatus.Internal\npublic class ListOptionContainer<T> extends CollapsibleContainer implements OptionValueProvider {\n\n    protected final Option<List<T>> backingOption;\n    protected final List<T> backingList;\n\n    protected final Button resetButton;\n\n    @SuppressWarnings(\"unchecked\")\n    public ListOptionContainer(Option<List<T>> option) {\n        super(\n                Sizing.fill(100), Sizing.content(),\n                Component.translatable(\"text.config.\" + option.configName() + \".option.\" + option.key().asString()),\n                option.backingField().field().isAnnotationPresent(Expanded.class)\n        );\n\n        this.backingOption = option;\n        this.backingList = new ArrayList<>(option.value());\n\n        this.padding(this.padding.get().add(0, 5, 0, 0));\n\n        this.titleLayout.horizontalSizing(Sizing.fill(100));\n        this.titleLayout.verticalSizing(Sizing.fixed(30));\n        this.titleLayout.verticalAlignment(VerticalAlignment.CENTER);\n\n        if (!option.detached()) {\n            this.titleLayout.child(UIComponents.label(Component.translatable(\"text.owo.config.list.add_entry\").withStyle(ChatFormatting.GRAY)).<LabelComponent>configure(label -> {\n                label.cursorStyle(CursorStyle.HAND);\n\n                label.mouseEnter().subscribe(() -> label.text(label.text().copy().withStyle(style -> style.withColor(ChatFormatting.YELLOW))));\n                label.mouseLeave().subscribe(() -> label.text(label.text().copy().withStyle(style -> style.withColor(ChatFormatting.GRAY))));\n                label.mouseDown().subscribe((click, doubled) -> {\n                    UISounds.playInteractionSound();\n                    this.backingList.add((T) \"\");\n\n                    if (!this.expanded) this.toggleExpansion();\n                    this.refreshOptions();\n\n                    var lastEntry = (ParentUIComponent) this.collapsibleChildren.get(this.collapsibleChildren.size() - 1);\n                    this.focusHandler().focus(\n                            lastEntry.children().get(lastEntry.children().size() - 1),\n                            FocusSource.MOUSE_CLICK\n                    );\n\n                    return true;\n                });\n            }));\n        }\n\n        this.resetButton = UIComponents.button(Component.literal(\"⇄\"), (ButtonComponent button) -> {\n            this.backingList.clear();\n            this.backingList.addAll(option.defaultValue());\n\n            this.refreshOptions();\n            button.active = false;\n        });\n        this.resetButton.margins(Insets.right(10));\n        this.resetButton.positioning(Positioning.relative(100, 50));\n        this.titleLayout.child(resetButton);\n        this.refreshResetButton();\n\n        this.refreshOptions();\n\n        this.titleLayout.child(new SearchAnchorComponent(\n                this.titleLayout,\n                option.key(),\n                () -> I18n.get(\"text.config.\" + option.configName() + \".option.\" + option.key().asString()),\n                () -> this.backingList.stream().map(Objects::toString).collect(Collectors.joining())\n        ));\n    }\n\n    @SuppressWarnings({\"unchecked\", \"ConstantConditions\"})\n    protected void refreshOptions() {\n        this.collapsibleChildren.clear();\n\n        var listType = ReflectionUtils.getTypeArgument(this.backingOption.backingField().field().getGenericType(), 0);\n        for (int i = 0; i < this.backingList.size(); i++) {\n            var container = UIContainers.horizontalFlow(Sizing.fill(100), Sizing.content());\n            container.verticalAlignment(VerticalAlignment.CENTER);\n\n            int optionIndex = i;\n            final var label = UIComponents.label(TextOps.withFormatting(\"- \", ChatFormatting.GRAY));\n            label.margins(Insets.left(10));\n            if (!this.backingOption.detached()) {\n                label.cursorStyle(CursorStyle.HAND);\n                label.mouseEnter().subscribe(() -> label.text(TextOps.withFormatting(\"x \", ChatFormatting.GRAY)));\n                label.mouseLeave().subscribe(() -> label.text(TextOps.withFormatting(\"- \", ChatFormatting.GRAY)));\n                label.mouseDown().subscribe((click, doubled) -> {\n                    this.backingList.remove(optionIndex);\n                    this.refreshResetButton();\n                    this.refreshOptions();\n                    UISounds.playInteractionSound();\n\n                    return true;\n                });\n            }\n            container.child(label);\n\n            final var box = new ConfigTextBox();\n            box.setValue(this.backingList.get(i).toString());\n            box.moveCursorToStart(false);\n            box.setBordered(false);\n            box.margins(Insets.vertical(2));\n            box.horizontalSizing(Sizing.fill(95));\n            box.verticalSizing(Sizing.fixed(8));\n\n            if (!this.backingOption.detached()) {\n                box.onChanged().subscribe(s -> {\n                    if (!box.isValid()) return;\n\n                    this.backingList.set(optionIndex, (T) box.parsedValue());\n                    this.refreshResetButton();\n                });\n            } else {\n                box.active = false;\n            }\n\n            if (NumberReflection.isNumberType(listType)) {\n                box.configureForNumber((Class<? extends Number>) listType);\n            }\n\n            container.child(box);\n            this.collapsibleChildren.add(container);\n        }\n\n        this.contentLayout.<FlowLayout>configure(layout -> {\n            layout.clearChildren();\n            if (this.expanded) layout.children(this.collapsibleChildren);\n        });\n        this.refreshResetButton();\n    }\n\n    protected void refreshResetButton() {\n        this.resetButton.active = !this.backingOption.detached() && !this.backingList.equals(this.backingOption.defaultValue());\n    }\n\n    @Override\n    public boolean shouldDrawTooltip(double mouseX, double mouseY) {\n        return ((mouseY - this.y) <= this.titleLayout.height()) && super.shouldDrawTooltip(mouseX, mouseY);\n    }\n\n    @Override\n    public boolean isValid() {\n        return true;\n    }\n\n    @Override\n    public Object parsedValue() {\n        return this.backingList;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/component/OptionValueProvider.java",
    "content": "package io.wispforest.owo.config.ui.component;\n\npublic interface OptionValueProvider {\n\n    /**\n     * @return {@code true} if the current state of this component\n     * describes a valid value for the option it is linked to\n     */\n    boolean isValid();\n\n    /**\n     * @return The value described by the current state\n     * of this component\n     */\n    Object parsedValue();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/config/ui/component/SearchAnchorComponent.java",
    "content": "package io.wispforest.owo.config.ui.component;\n\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.config.ui.ConfigScreen;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Positioning;\nimport io.wispforest.owo.ui.core.Sizing;\n\nimport java.util.Arrays;\nimport java.util.Locale;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class SearchAnchorComponent extends BaseUIComponent {\n\n    protected final ParentUIComponent anchorFrame;\n    protected final Supplier<String>[] searchTextSources;\n    protected final Option.Key key;\n\n    protected Consumer<ConfigScreen.SearchHighlighterComponent> highlightConfigurator = highlight -> {};\n\n    @SafeVarargs\n    public SearchAnchorComponent(ParentUIComponent anchorFrame, Option.Key key, Supplier<String>... searchTextSources) {\n        this.anchorFrame = anchorFrame;\n        this.searchTextSources = searchTextSources;\n        this.key = key;\n\n        this.positioning(Positioning.absolute(0, 0));\n        this.sizing(Sizing.fixed(0));\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {}\n\n    public ParentUIComponent anchorFrame() {\n        return this.anchorFrame;\n    }\n\n    public ConfigScreen.SearchHighlighterComponent configure(ConfigScreen.SearchHighlighterComponent component) {\n        this.highlightConfigurator.accept(component);\n        return component;\n    }\n\n    public SearchAnchorComponent highlightConfigurator(Consumer<ConfigScreen.SearchHighlighterComponent> highlightConfigurator) {\n        this.highlightConfigurator = highlightConfigurator;\n        return this;\n    }\n\n    public Option.Key key() {\n        return this.key;\n    }\n\n    public String currentSearchText() {\n        return Arrays.stream(this.searchTextSources)\n                .map(Supplier::get)\n                .map(s -> s.toLowerCase(Locale.ROOT))\n                .collect(Collectors.joining());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ext/DerivedComponentMap.java",
    "content": "package io.wispforest.owo.ext;\n\nimport net.minecraft.core.component.DataComponentMap;\nimport net.minecraft.core.component.DataComponentPatch;\nimport net.minecraft.core.component.DataComponentType;\nimport net.minecraft.core.component.PatchedDataComponentMap;\nimport net.minecraft.world.item.ItemStack;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Objects;\nimport java.util.Set;\n\n@ApiStatus.Internal\npublic class DerivedComponentMap implements DataComponentMap {\n    private final DataComponentMap base;\n    private final PatchedDataComponentMap delegate;\n\n    public DerivedComponentMap(DataComponentMap base) {\n        this.base = base;\n        this.delegate = new PatchedDataComponentMap(base);\n    }\n\n    public static DataComponentMap reWrapIfNeeded(DataComponentMap original) {\n        if (original instanceof DerivedComponentMap derived) {\n            return new DerivedComponentMap(derived.base);\n        } else {\n            return original;\n        }\n    }\n\n    public void derive(ItemStack owner) {\n        delegate.restorePatch(DataComponentPatch.EMPTY);\n        var builder = DataComponentPatch.builder();\n        owner.getItem().deriveStackComponents(owner.getComponents(), builder);\n        delegate.restorePatch(builder.build());\n    }\n\n    @Nullable\n    @Override\n    public <T> T get(DataComponentType<? extends T> type) {\n        return delegate.get(type);\n    }\n\n    @Override\n    public Set<DataComponentType<?>> keySet() {\n        return delegate.keySet();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        } else if (o instanceof DerivedComponentMap thatDerived) {\n            return Objects.equals(base, thatDerived.base);\n        } else if (o instanceof DataComponentMap.Builder.SimpleMap simpleComponentMap) {\n            return Objects.equals(base, simpleComponentMap);\n        }\n\n        return o == EMPTY && this.base == EMPTY;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hashCode(base);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ext/OwoItem.java",
    "content": "package io.wispforest.owo.ext;\n\nimport net.minecraft.core.component.DataComponentMap;\nimport net.minecraft.core.component.DataComponentPatch;\nimport org.jetbrains.annotations.ApiStatus;\n\npublic interface OwoItem {\n    /**\n     * Generates component-derived-components from the stack's components\n     * @param source a map containing the item stack's non-derived components\n     * @param target a builder for the derived component map\n     */\n    @ApiStatus.Experimental\n    default void deriveStackComponents(DataComponentMap source, DataComponentPatch.Builder target) { }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/Icon.java",
    "content": "package io.wispforest.owo.itemgroup;\n\nimport io.wispforest.owo.client.texture.AnimatedTextureDrawable;\nimport io.wispforest.owo.client.texture.SpriteSheetMetadata;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.ItemLike;\n\n/**\n * An icon used for rendering on buttons in {@link OwoItemGroup}s\n * <p>\n * Default implementations provided for textures and item stacks\n */\n@FunctionalInterface\npublic interface Icon {\n\n    @Environment(EnvType.CLIENT)\n    void render(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float delta);\n\n    static Icon of(ItemStack stack) {\n        return new Icon() {\n            @Override\n            public void render(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float delta) {\n                graphics.renderFakeItem(stack, x, y);\n            }\n        };\n    }\n\n    static Icon of(ItemLike item) {\n        return of(new ItemStack(item));\n    }\n\n    static Icon of(Identifier texture, int u, int v, int textureWidth, int textureHeight) {\n        return new Icon() {\n            @Override\n            public void render(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float delta) {\n                graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, 16, 16, textureWidth, textureHeight);\n            }\n        };\n    }\n\n    /**\n     * Creates an Animated ItemGroup Icon\n     *\n     * @param texture     The texture to render, this is the spritesheet\n     * @param textureSize The size of the texture, it is assumed to be square\n     * @param frameDelay  The delay in milliseconds between frames.\n     * @param loop        Should the animation play once or loop?\n     * @return The created icon instance\n     */\n    static Icon of(Identifier texture, int textureSize, int frameDelay, boolean loop) {\n        var widget = new AnimatedTextureDrawable(0, 0, 16, 16, texture, new SpriteSheetMetadata(textureSize, 16), frameDelay, loop);\n        return new Icon() {\n            @Override\n            public void render(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float delta) {\n                widget.render(x, y, graphics, mouseX, mouseY, delta);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/ItemGroupReference.java",
    "content": "package io.wispforest.owo.itemgroup;\n\npublic record ItemGroupReference(OwoItemGroup group, int tab) {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/OwoItemGroup.java",
    "content": "package io.wispforest.owo.itemgroup;\n\nimport io.wispforest.owo.itemgroup.gui.ItemGroupButton;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupButtonWidget;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupTab;\nimport io.wispforest.owo.mixin.itemgroup.CreativeModeTabAccessor;\nimport io.wispforest.owo.util.pond.OwoItemExtensions;\nimport it.unimi.dsi.fastutil.ints.IntAVLTreeSet;\nimport it.unimi.dsi.fastutil.ints.IntComparators;\nimport it.unimi.dsi.fastutil.ints.IntSet;\nimport it.unimi.dsi.fastutil.ints.IntSets;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.flag.FeatureFlagSet;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.world.level.ItemLike;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\n/**\n * Extensions for  {@link CreativeModeTab} which support multiple sub-tabs\n * within, as well as arbitrary buttons with defaults provided for links\n * to places like GitHub, Modrinth, etc.\n * <p>\n * Tabs can be populated by setting the {@link OwoItemSettingsExtension#tab(int)}.\n * Furthermore, tags can be used for easily populating tabs from data\n * <p>\n * The roots of this implementation originated in Biome Makeover, where it was written by Lemonszz\n */\npublic abstract class OwoItemGroup extends CreativeModeTab {\n\n    public static final BiConsumer<Item, Output> DEFAULT_STACK_GENERATOR = (item, stacks) -> stacks.accept(item.getDefaultInstance());\n\n    protected static final ItemGroupTab PLACEHOLDER_TAB = new ItemGroupTab(Icon.of(Items.AIR), Component.empty(), (br, uh) -> {}, ItemGroupTab.DEFAULT_TEXTURE, false);\n\n    public final List<ItemGroupTab> tabs = new ArrayList<>();\n    public final List<ItemGroupButton> buttons = new ArrayList<>();\n\n    private final Consumer<OwoItemGroup> initializer;\n\n    private final Supplier<Icon> iconSupplier;\n    private Icon icon;\n\n    private final IntSet activeTabs = new IntAVLTreeSet(IntComparators.NATURAL_COMPARATOR);\n    private final IntSet activeTabsView = IntSets.unmodifiable(this.activeTabs);\n    private boolean initialized = false;\n\n    private final @Nullable Identifier backgroundTexture;\n    private final @Nullable ScrollerTextures scrollerTextures;\n    private final @Nullable TabTextures tabTextures;\n\n    private final int tabStackHeight;\n    private final int buttonStackHeight;\n    private final boolean useDynamicTitle;\n    private final boolean displaySingleTab;\n    private final boolean allowMultiSelect;\n\n    protected OwoItemGroup(Identifier id, Consumer<OwoItemGroup> initializer, Supplier<Icon> iconSupplier, int tabStackHeight, int buttonStackHeight, @Nullable Identifier backgroundTexture, @Nullable ScrollerTextures scrollerTextures, @Nullable TabTextures tabTextures, boolean useDynamicTitle, boolean displaySingleTab, boolean allowMultiSelect) {\n        super(null, -1, Type.CATEGORY, Component.translatable(\"itemGroup.%s.%s\".formatted(id.getNamespace(), id.getPath())), () -> ItemStack.EMPTY, (displayContext, entries) -> {});\n        this.initializer = initializer;\n        this.iconSupplier = iconSupplier;\n        this.tabStackHeight = tabStackHeight;\n        this.buttonStackHeight = buttonStackHeight;\n        this.backgroundTexture = backgroundTexture;\n        this.scrollerTextures = scrollerTextures;\n        this.tabTextures = tabTextures;\n        this.useDynamicTitle = useDynamicTitle;\n        this.displaySingleTab = displaySingleTab;\n        this.allowMultiSelect = allowMultiSelect;\n\n        ((CreativeModeTabAccessor) this).owo$setDisplayItemsGenerator((context, entries) -> {\n            if (!this.initialized) {\n                throw new IllegalStateException(\"oωo item group not initialized, was 'initialize()' called?\");\n            }\n\n            this.activeTabs.forEach(tabIdx -> {\n                this.tabs.get(tabIdx).contentSupplier().addItems(context, entries);\n                this.collectItemsFromRegistry(entries, tabIdx);\n            });\n        });\n    }\n\n    public static Builder builder(Identifier id, Supplier<Icon> iconSupplier) {\n        return new Builder(id, iconSupplier);\n    }\n\n    // ---------\n\n    /**\n     * Executes {@link #initializer} and makes sure this item group is ready for use\n     * <p>\n     * Call this after all of your items have been registered to make sure your icons\n     * show up correctly\n     */\n    public void initialize() {\n        if (this.initialized) return;\n\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) this.initializer.accept(this);\n        if (this.tabs.isEmpty()) this.tabs.add(PLACEHOLDER_TAB);\n\n        if (this.allowMultiSelect) {\n            for (int tabIdx = 0; tabIdx < this.tabs.size(); tabIdx++) {\n                if (!this.tabs.get(tabIdx).primary()) continue;\n                this.activeTabs.add(tabIdx);\n            }\n\n            if (this.activeTabs.isEmpty()) this.activeTabs.add(0);\n        } else {\n            this.activeTabs.add(0);\n        }\n\n        this.initialized = true;\n    }\n\n    /**\n     * Adds the specified button to the buttons on\n     * the right side of the creative menu\n     *\n     * @param button The button to add\n     * @see ItemGroupButton#link(CreativeModeTab, Icon, String, String)\n     * @see ItemGroupButton#curseforge(CreativeModeTab, String)\n     * @see ItemGroupButton#discord(CreativeModeTab, String)\n     */\n    public void addButton(ItemGroupButton button) {\n        this.buttons.add(button);\n    }\n\n    /**\n     * Adds a new tab to this group\n     *\n     * @param icon       The icon to use\n     * @param name       The name of the tab, used for the translation key\n     * @param contentTag The tag used for filling this tab\n     * @param texture    The texture to use for drawing the button\n     * @see Icon#of(ItemLike)\n     */\n    public void addTab(Icon icon, String name, @Nullable TagKey<Item> contentTag, Identifier texture, boolean primary) {\n        this.tabs.add(new ItemGroupTab(\n                icon,\n                ButtonDefinition.tooltipFor(this, \"tab\", name),\n                contentTag == null\n                        ? (context, entries) -> {}\n                        : (context, entries) -> BuiltInRegistries.ITEM.stream().filter(item -> item.builtInRegistryHolder().is(contentTag)).forEach(entries::accept),\n                texture,\n                primary\n        ));\n    }\n\n    /**\n     * Adds a new tab to this group, using the default button texture\n     *\n     * @param icon       The icon to use\n     * @param name       The name of the tab, used for the translation key\n     * @param contentTag The tag used for filling this tab\n     * @see Icon#of(ItemLike)\n     */\n    public void addTab(Icon icon, String name, @Nullable TagKey<Item> contentTag, boolean primary) {\n        addTab(icon, name, contentTag, ItemGroupTab.DEFAULT_TEXTURE, primary);\n    }\n\n    /**\n     * Adds a new tab to this group, using the default button texture\n     *\n     * @param icon            The icon to use\n     * @param name            The name of the tab, used for the translation key\n     * @param contentSupplier The function used for filling this tab\n     * @param texture         The texture to use for drawing the button\n     * @see Icon#of(ItemLike)\n     */\n    public void addCustomTab(Icon icon, String name, ItemGroupTab.ContentSupplier contentSupplier, Identifier texture, boolean primary) {\n        this.tabs.add(new ItemGroupTab(\n                icon,\n                ButtonDefinition.tooltipFor(this, \"tab\", name),\n                contentSupplier, texture, primary\n        ));\n    }\n\n    /**\n     * Adds a new tab to this group\n     *\n     * @param icon            The icon to use\n     * @param name            The name of the tab, used for the translation key\n     * @param contentSupplier The function used for filling this tab\n     * @see Icon#of(ItemLike)\n     */\n    public void addCustomTab(Icon icon, String name, ItemGroupTab.ContentSupplier contentSupplier, boolean primary) {\n        this.addCustomTab(icon, name, contentSupplier, ItemGroupTab.DEFAULT_TEXTURE, primary);\n    }\n\n    @Override\n    public void buildContents(ItemDisplayParameters context) {\n        super.buildContents(context);\n\n        var searchEntries = new SearchOnlyEntries(this, context.enabledFeatures());\n\n        this.collectItemsFromRegistry(searchEntries, -1);\n        this.tabs.forEach(tab -> tab.contentSupplier().addItems(context, searchEntries));\n\n        ((CreativeModeTabAccessor) this).owo$setDisplayItemsSearchTab(searchEntries.searchTabContents);\n    }\n\n    protected void collectItemsFromRegistry(Output entries, int tab) {\n        BuiltInRegistries.ITEM.stream()\n                .filter(item -> ((OwoItemExtensions) item).owo$group() == this && (tab < 0 || tab == ((OwoItemExtensions) item).owo$tab()))\n                .forEach(item -> ((OwoItemExtensions) item).owo$stackGenerator().accept(item, entries));\n    }\n\n    // Getters and setters\n\n    /**\n     * Select only {@code tab}, deselecting all other tabs,\n     * using {@code context} for re-population\n     */\n    public void selectSingleTab(int tab, ItemDisplayParameters context) {\n        this.activeTabs.clear();\n        this.activeTabs.add(tab);\n\n        this.buildContents(context);\n    }\n\n    /**\n     * Select {@code tab} in addition to other currently selected\n     * tabs, using {@code context} for re-population.\n     * <p>\n     * If this group does not allow multiple selection, behaves\n     * like {@link #selectSingleTab(int, ItemDisplayParameters)}\n     */\n    public void selectTab(int tab, ItemDisplayParameters context) {\n        if (!this.allowMultiSelect) {\n            this.activeTabs.clear();\n        }\n\n        this.activeTabs.add(tab);\n        this.buildContents(context);\n    }\n\n    /**\n     * Deselect {@code tab} if it is currently selected, using {@code context} for\n     * re-population. If this results in no tabs being selected, all tabs are\n     * automatically selected instead\n     */\n    public void deselectTab(int tab, ItemDisplayParameters context) {\n        if (!this.allowMultiSelect) return;\n\n        this.activeTabs.remove(tab);\n        if (this.activeTabs.isEmpty()) {\n            for (int tabIdx = 0; tabIdx < this.tabs.size(); tabIdx++) {\n                this.activeTabs.add(tabIdx);\n            }\n        }\n\n        this.buildContents(context);\n    }\n\n    /**\n     * Shorthand for {@link #selectTab(int, ItemDisplayParameters)} or\n     * {@link #deselectTab(int, ItemDisplayParameters)}, depending on the tabs\n     * current state\n     */\n    public void toggleTab(int tab, ItemDisplayParameters context) {\n        if (this.isTabSelected(tab)) {\n            this.deselectTab(tab, context);\n        } else {\n            this.selectTab(tab, context);\n        }\n    }\n\n    /**\n     * @return A set containing the indices of all currently\n     * selected tabs\n     */\n    public IntSet selectedTabs() {\n        return this.activeTabsView;\n    }\n\n    /**\n     * @return {@code true} if {@code tab} is currently selected\n     */\n    public boolean isTabSelected(int tab) {\n        return this.activeTabs.contains(tab);\n    }\n\n    public @Nullable Identifier getOwoBackgroundTexture() {\n        return this.backgroundTexture;\n    }\n\n    public @Nullable ScrollerTextures getScrollerTextures() {\n        return this.scrollerTextures;\n    }\n\n    public @Nullable TabTextures getTabTextures() {\n        return this.tabTextures;\n    }\n\n    public int getTabStackHeight() {\n        return tabStackHeight;\n    }\n\n    public int getButtonStackHeight() {\n        return buttonStackHeight;\n    }\n\n    public boolean hasDynamicTitle() {\n        return this.useDynamicTitle && (this.tabs.size() > 1 || this.shouldDisplaySingleTab());\n    }\n\n    public boolean shouldDisplaySingleTab() {\n        return this.displaySingleTab;\n    }\n\n    public boolean canSelectMultipleTabs() {\n        return this.allowMultiSelect;\n    }\n\n    public List<ItemGroupButton> getButtons() {\n        return buttons;\n    }\n\n    public ItemGroupTab getTab(int index) {\n        return index < this.tabs.size() ? this.tabs.get(index) : null;\n    }\n\n    public Icon icon() {\n        return this.icon == null\n                ? this.icon = this.iconSupplier.get()\n                : this.icon;\n    }\n\n    @Override\n    public boolean shouldDisplay() {\n        return true;\n    }\n\n    public Identifier id() {\n        return BuiltInRegistries.CREATIVE_MODE_TAB.getKey(this);\n    }\n\n    public static class Builder {\n\n        private final Identifier id;\n        private final Supplier<Icon> iconSupplier;\n\n        private Consumer<OwoItemGroup> initializer = owoItemGroup -> {};\n        private int tabStackHeight = 4;\n        private int buttonStackHeight = 4;\n        private @Nullable Identifier backgroundTexture = null;\n        private @Nullable ScrollerTextures scrollerTextures = null;\n        private @Nullable TabTextures tabTextures = null;\n        private boolean useDynamicTitle = true;\n        private boolean displaySingleTab = false;\n        private boolean allowMultiSelect = true;\n\n        private Builder(Identifier id, Supplier<Icon> iconSupplier) {\n            this.id = id;\n            this.iconSupplier = iconSupplier;\n        }\n\n        public Builder initializer(Consumer<OwoItemGroup> initializer) {\n            this.initializer = initializer;\n            return this;\n        }\n\n        public Builder tabStackHeight(int tabStackHeight) {\n            this.tabStackHeight = tabStackHeight;\n            return this;\n        }\n\n        public Builder buttonStackHeight(int buttonStackHeight) {\n            this.buttonStackHeight = buttonStackHeight;\n            return this;\n        }\n\n        public Builder backgroundTexture(@Nullable Identifier backgroundTexture) {\n            this.backgroundTexture = backgroundTexture;\n            return this;\n        }\n\n        public Builder scrollerTextures(ScrollerTextures scrollerTextures) {\n            this.scrollerTextures = scrollerTextures;\n            return this;\n        }\n\n        public Builder tabTextures(TabTextures tabTextures) {\n            this.tabTextures = tabTextures;\n            return this;\n        }\n\n        public Builder disableDynamicTitle() {\n            this.useDynamicTitle = false;\n            return this;\n        }\n\n        public Builder displaySingleTab() {\n            this.displaySingleTab = true;\n            return this;\n        }\n\n        public Builder withoutMultipleSelection() {\n            this.allowMultiSelect = false;\n            return this;\n        }\n\n        public OwoItemGroup build() {\n            final var group = new OwoItemGroup(id, initializer, iconSupplier, tabStackHeight, buttonStackHeight, backgroundTexture, scrollerTextures, tabTextures, useDynamicTitle, displaySingleTab, allowMultiSelect) {};\n            Registry.register(BuiltInRegistries.CREATIVE_MODE_TAB, this.id, group);\n            return group;\n        }\n    }\n\n    protected static class SearchOnlyEntries extends ItemDisplayBuilder {\n\n        public SearchOnlyEntries(CreativeModeTab group, FeatureFlagSet enabledFeatures) {\n            super(group, enabledFeatures);\n        }\n\n        @Override\n        public void accept(ItemStack stack, TabVisibility visibility) {\n            if (visibility == TabVisibility.PARENT_TAB_ONLY) return;\n            super.accept(stack, TabVisibility.SEARCH_TAB_ONLY);\n        }\n    }\n\n    public record ScrollerTextures(Identifier enabled, Identifier disabled) {}\n    public record TabTextures(Identifier topSelected, Identifier topSelectedFirstColumn, Identifier topUnselected, Identifier bottomSelected, Identifier bottomSelectedFirstColumn, Identifier bottomUnselected) {}\n\n    // Utility\n\n    /**\n     * Defines a button's appearance and translation key\n     * <p>\n     * Used by {@link ItemGroupButtonWidget}\n     */\n    public interface ButtonDefinition {\n\n        Icon icon();\n\n        Identifier texture();\n\n        Component tooltip();\n\n        static Component tooltipFor(CreativeModeTab group, String component, String componentName) {\n            var registryId = BuiltInRegistries.CREATIVE_MODE_TAB.getKey(group);\n            var groupId = registryId.getNamespace().equals(\"minecraft\")\n                    ? registryId.getPath()\n                    : registryId.getNamespace() + \".\" + registryId.getPath();\n\n            return Component.translatable(\"itemGroup.\" + groupId + \".\" + component + \".\" + componentName);\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/OwoItemSettingsExtension.java",
    "content": "package io.wispforest.owo.itemgroup;\n\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.level.Level;\n\nimport java.util.function.BiConsumer;\n\npublic interface OwoItemSettingsExtension {\n\n    default Item.Properties group(ItemGroupReference ref) {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    /**\n     * @param group The item group this item should appear in\n     */\n    default Item.Properties group(OwoItemGroup group) {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    default OwoItemGroup group() {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    default Item.Properties tab(int tab) {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    default int tab() {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    /**\n     * @param generator The function this item uses for creating stacks in the\n     *                  {@link OwoItemGroup} it is in, by default this will be {@link OwoItemGroup#DEFAULT_STACK_GENERATOR}\n     */\n    default Item.Properties stackGenerator(BiConsumer<Item, CreativeModeTab.Output> generator) {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    default BiConsumer<Item, CreativeModeTab.Output> stackGenerator() {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    /**\n     * Automatically increment {@link net.minecraft.stats.Stats#ITEM_USED}\n     * for this item every time {@link Item#use(Level, Player, InteractionHand)}\n     * returns an accepted result\n     */\n    default Item.Properties trackUsageStat() {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n\n    default boolean shouldTrackUsageStat() {\n        throw new IllegalStateException(\"Implemented in mixin.\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/gui/ItemGroupButton.java",
    "content": "package io.wispforest.owo.itemgroup.gui;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.itemgroup.Icon;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.ConfirmLinkScreen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.Util;\nimport net.minecraft.world.item.CreativeModeTab;\n\n/**\n * A button placed to the right side of the creative inventory. Provides defaults\n * for linking to sites, but can execute arbitrary actions\n */\npublic final class ItemGroupButton implements OwoItemGroup.ButtonDefinition {\n\n    public static final Identifier ICONS_TEXTURE = Owo.id(\"textures/gui/icons.png\");\n\n    private final Icon icon;\n    private final Component tooltip;\n    private final Identifier texture;\n    private final Runnable action;\n\n    public ItemGroupButton(CreativeModeTab group, Icon icon, String name, Identifier texture, Runnable action) {\n        this.icon = icon;\n        this.tooltip = OwoItemGroup.ButtonDefinition.tooltipFor(group, \"button\", name);\n        this.action = action;\n        this.texture = texture;\n    }\n\n    public ItemGroupButton(CreativeModeTab group, Icon icon, String name, Runnable action) {\n        this(group, icon, name, ItemGroupTab.DEFAULT_TEXTURE, action);\n    }\n\n    public static ItemGroupButton github(CreativeModeTab group, String url) {\n        return link(group, Icon.of(ICONS_TEXTURE, 0, 0, 64, 64), \"github\", url);\n    }\n\n    public static ItemGroupButton modrinth(CreativeModeTab group, String url) {\n        return link(group, Icon.of(ICONS_TEXTURE, 16, 0, 64, 64), \"modrinth\", url);\n    }\n\n    public static ItemGroupButton curseforge(CreativeModeTab group, String url) {\n        return link(group, Icon.of(ICONS_TEXTURE, 32, 0, 64, 64), \"curseforge\", url);\n    }\n\n    public static ItemGroupButton discord(CreativeModeTab group, String url) {\n        return link(group, Icon.of(ICONS_TEXTURE, 48, 0, 64, 64), \"discord\", url);\n    }\n\n    /**\n     * Creates a button that opens the given link when clicked\n     *\n     * @param icon The icon for this button to use\n     * @param name The name of this button, used for the translation key\n     * @param url  The url to open\n     * @return The created button\n     */\n    public static ItemGroupButton link(CreativeModeTab group, Icon icon, String name, String url) {\n        return new ItemGroupButton(group, icon, name, () -> {\n            final var client = Minecraft.getInstance();\n            var screen = client.screen;\n            client.setScreen(new ConfirmLinkScreen(confirmed -> {\n                if (confirmed) Util.getPlatform().openUri(url);\n                client.setScreen(screen);\n            }, url, true));\n        });\n    }\n\n    @Override\n    public Identifier texture() {\n        return this.texture;\n    }\n\n    @Override\n    public Icon icon() {\n        return this.icon;\n    }\n\n    @Override\n    public Component tooltip() {\n        return this.tooltip;\n    }\n\n    public Runnable action() {\n        return this.action;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/gui/ItemGroupButtonWidget.java",
    "content": "package io.wispforest.owo.itemgroup.gui;\n\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.function.Consumer;\n\n@ApiStatus.Internal\npublic class ItemGroupButtonWidget extends Button {\n\n    public boolean isSelected = false;\n    private final OwoItemGroup.ButtonDefinition definition;\n    private final int baseU;\n\n    public ItemGroupButtonWidget(int x, int y, int baseU, OwoItemGroup.ButtonDefinition definition, Consumer<ItemGroupButtonWidget> onPress) {\n        super(x, y, 24, 24, definition.tooltip(), button -> onPress.accept((ItemGroupButtonWidget) button), Button.DEFAULT_NARRATION);\n        this.baseU = baseU;\n        this.definition = definition;\n    }\n\n    @Override\n    public void renderContents(GuiGraphics context, int mouseX, int mouseY, float delta) {\n        context.blit(RenderPipelines.GUI_TEXTURED, this.definition.texture(), this.getX(), this.getY(), this.baseU, this.isHoveredOrFocused() || this.isSelected ? this.height : 0, this.width, this.height, 64, 64);\n\n        this.definition.icon().render(context, this.getX() + 4, this.getY() + 4, mouseX, mouseY, delta);\n    }\n\n    public boolean isTab() {\n        return this.definition instanceof ItemGroupTab;\n    }\n\n    public boolean trulyHovered() {\n        return this.isHovered;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/gui/ItemGroupTab.java",
    "content": "package io.wispforest.owo.itemgroup.gui;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.itemgroup.Icon;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.itemgroup.OwoItemSettingsExtension;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.item.CreativeModeTab;\n\n/**\n * Represents a tab inside an {@link OwoItemGroup} that contains all items in the\n * passed {@code contentTag}. If you want to use {@link OwoItemSettingsExtension#tab(int)} to\n * define the contents, use {@code null} as the tag\n */\npublic record ItemGroupTab(\n    Icon icon,\n    Component name,\n    ContentSupplier contentSupplier,\n    Identifier texture,\n    boolean primary\n) implements OwoItemGroup.ButtonDefinition {\n\n    public static final Identifier DEFAULT_TEXTURE = Owo.id(\"textures/gui/tabs.png\");\n\n    @Override\n    public Component tooltip() {\n        return this.name;\n    }\n\n    @FunctionalInterface\n    public interface ContentSupplier {\n        void addItems(CreativeModeTab.ItemDisplayParameters context, CreativeModeTab.Output entries);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/json/OwoItemGroupLoader.java",
    "content": "package io.wispforest.owo.itemgroup.json;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport io.wispforest.owo.itemgroup.Icon;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupButton;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupTab;\nimport io.wispforest.owo.moddata.ModDataConsumer;\nimport io.wispforest.owo.util.pond.OwoItemExtensions;\nimport net.fabricmc.fabric.api.event.registry.RegistryEntryAddedCallback;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.util.GsonHelper;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.CreativeModeTabs;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Manages loading and adding JSON-based tabs to preexisting {@code ItemGroup}s\n * without needing to depend on owo\n * <p>\n * This is used instead of a {@link net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener} because\n * it needs to load on the client\n */\n@ApiStatus.Internal\npublic class OwoItemGroupLoader implements ModDataConsumer {\n\n    public static final OwoItemGroupLoader INSTANCE = new OwoItemGroupLoader();\n\n    private static final Map<Identifier, JsonObject> BUFFERED_GROUPS = new HashMap<>();\n\n    private OwoItemGroupLoader() {}\n\n    public static void onGroupCreated(CreativeModeTab group) {\n        var groupId = BuiltInRegistries.CREATIVE_MODE_TAB.getKey(group);\n\n        if (!BUFFERED_GROUPS.containsKey(groupId)) return;\n        INSTANCE.acceptParsedFile(groupId, BUFFERED_GROUPS.remove(groupId));\n    }\n\n    @Override\n    public void acceptParsedFile(Identifier id, JsonObject json) {\n        var targetGroupId = Identifier.parse(GsonHelper.getAsString(json, \"target_group\"));\n\n        CreativeModeTab searchGroup = null;\n        for (CreativeModeTab group : CreativeModeTabs.allTabs()) {\n            if (BuiltInRegistries.CREATIVE_MODE_TAB.getKey(group).equals(targetGroupId)) {\n                searchGroup = group;\n                break;\n            }\n        }\n\n        if (searchGroup == null) {\n            BUFFERED_GROUPS.put(targetGroupId, json);\n            return;\n        }\n\n        final var targetGroup = searchGroup;\n\n        var tabsArray = GsonHelper.getAsJsonArray(json, \"tabs\", new JsonArray());\n        var tabs = new ArrayList<ItemGroupTab>();\n\n        tabsArray.forEach(jsonElement -> {\n            if (!jsonElement.isJsonObject()) return;\n            var tabObject = jsonElement.getAsJsonObject();\n\n            var texture = Identifier.parse(GsonHelper.getAsString(tabObject, \"texture\", ItemGroupTab.DEFAULT_TEXTURE.toString()));\n\n            var tag = TagKey.create(Registries.ITEM, Identifier.parse(GsonHelper.getAsString(tabObject, \"tag\")));\n            var icon = BuiltInRegistries.ITEM.getValue(Identifier.parse(GsonHelper.getAsString(tabObject, \"icon\")));\n            var name = GsonHelper.getAsString(tabObject, \"name\");\n\n            tabs.add(new ItemGroupTab(\n                    Icon.of(icon),\n                    OwoItemGroup.ButtonDefinition.tooltipFor(targetGroup, \"tab\", name),\n                    (context, entries) -> BuiltInRegistries.ITEM.stream().filter(item -> item.builtInRegistryHolder().is(tag)).forEach(entries::accept),\n                    texture,\n                    false\n            ));\n        });\n\n        var buttonsArray = GsonHelper.getAsJsonArray(json, \"buttons\", new JsonArray());\n        var buttons = new ArrayList<ItemGroupButton>();\n\n        buttonsArray.forEach(jsonElement -> {\n            if (!jsonElement.isJsonObject()) return;\n            var buttonObject = jsonElement.getAsJsonObject();\n\n            String link = GsonHelper.getAsString(buttonObject, \"link\");\n            String name = GsonHelper.getAsString(buttonObject, \"name\");\n\n            int u = GsonHelper.getAsInt(buttonObject, \"texture_u\");\n            int v = GsonHelper.getAsInt(buttonObject, \"texture_v\");\n\n            int textureWidth = GsonHelper.getAsInt(buttonObject, \"texture_width\", 64);\n            int textureHeight = GsonHelper.getAsInt(buttonObject, \"texture_height\", 64);\n\n            final var textureId = GsonHelper.getAsString(buttonObject, \"texture\", null);\n            var texture = textureId == null\n                    ? ItemGroupButton.ICONS_TEXTURE\n                    : Identifier.parse(textureId);\n\n            buttons.add(ItemGroupButton.link(targetGroup, Icon.of(texture, u, v, textureWidth, textureHeight), name, link));\n        });\n\n        if (targetGroup instanceof WrapperGroup wrapper) {\n            wrapper.addTabs(tabs);\n            wrapper.addButtons(buttons);\n\n            if (GsonHelper.getAsBoolean(json, \"extend\", false)) wrapper.markExtension();\n        } else {\n            var wrapper = new WrapperGroup(targetGroup, targetGroupId, tabs, buttons);\n            wrapper.initialize();\n            if (GsonHelper.getAsBoolean(json, \"extend\", false)) wrapper.markExtension();\n\n            BuiltInRegistries.ITEM.stream()\n                    .filter(item -> ((OwoItemExtensions) item).owo$group() == targetGroup)\n                    .forEach(item -> ((OwoItemExtensions) item).owo$setGroup(wrapper));\n        }\n    }\n\n    @Override\n    public String getDataSubdirectory() {\n        return \"item_group_tabs\";\n    }\n\n    static {\n        RegistryEntryAddedCallback.event(BuiltInRegistries.CREATIVE_MODE_TAB).register((rawId, id, group) -> {\n            OwoItemGroupLoader.onGroupCreated(group);\n        });\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/itemgroup/json/WrapperGroup.java",
    "content": "package io.wispforest.owo.itemgroup.json;\n\nimport io.wispforest.owo.itemgroup.Icon;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupButton;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupTab;\nimport io.wispforest.owo.mixin.itemgroup.CreativeModeTabAccessor;\nimport io.wispforest.owo.util.pond.OwoSimpleRegistryExtensions;\nimport net.minecraft.core.RegistrationInfo;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.world.item.CreativeModeTab;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * Used to replace a vanilla or modded item group to add the JSON-defined\n * tabs while keeping the same name, id and icon\n */\n@ApiStatus.Internal\npublic class WrapperGroup extends OwoItemGroup {\n\n    private final CreativeModeTab parent;\n    private boolean extension = false;\n\n    @SuppressWarnings(\"unchecked\")\n    public WrapperGroup(CreativeModeTab parent, Identifier parentId, List<ItemGroupTab> tabs, List<ItemGroupButton> buttons) {\n        super(parentId, owoItemGroup -> {}, () -> Icon.of(parent.getIconItem()), 4, 4, null, null, null, true, false, false);\n\n        int parentRawId = BuiltInRegistries.CREATIVE_MODE_TAB.getId(parent);\n\n        ((OwoSimpleRegistryExtensions<CreativeModeTab>) BuiltInRegistries.CREATIVE_MODE_TAB).owo$set(parentRawId, ResourceKey.create(Registries.CREATIVE_MODE_TAB, parentId), this, RegistrationInfo.BUILT_IN);\n\n        ((CreativeModeTabAccessor) this).owo$setDisplayName(parent.getDisplayName());\n        ((CreativeModeTabAccessor) this).owo$setColumn(parent.column());\n        ((CreativeModeTabAccessor) this).owo$setRow(parent.row());\n\n        this.parent = parent;\n\n        this.tabs.addAll(tabs);\n        this.buttons.addAll(buttons);\n    }\n\n    public void addTabs(Collection<ItemGroupTab> tabs) {\n        this.tabs.addAll(tabs);\n    }\n\n    public void addButtons(Collection<ItemGroupButton> buttons) {\n        this.buttons.addAll(buttons);\n    }\n\n    public void markExtension() {\n        if (this.extension) return;\n        this.extension = true;\n\n        if (this.tabs.get(0) == PLACEHOLDER_TAB) {\n            this.tabs.remove(0);\n        }\n\n        this.tabs.add(0, new ItemGroupTab(\n                Icon.of(this.parent.getIconItem()),\n                this.parent.getDisplayName(),\n                ((CreativeModeTabAccessor) this.parent).owo$getDisplayItemsGenerator()::accept,\n                ItemGroupTab.DEFAULT_TEXTURE,\n                true\n        ));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/AbstractContainerMenuInvoker.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.item.ItemStack;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(AbstractContainerMenu.class)\npublic interface AbstractContainerMenuInvoker {\n\n    @Invoker(\"moveItemStackTo\")\n    boolean owo$insertItem(ItemStack stack, int startIndex, int endIndex, boolean fromLast);\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/AbstractContainerMenuMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport io.wispforest.owo.client.screens.OwoAbstractContainerMenu;\nimport io.wispforest.owo.client.screens.MenuNetworkingInternals;\nimport io.wispforest.owo.client.screens.ScreenhandlerMessageData;\nimport io.wispforest.owo.client.screens.SyncedProperty;\nimport io.wispforest.owo.network.NetworkException;\nimport io.wispforest.owo.serialization.RegistriesAttribute;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.util.pond.OwoAbstractContainerMenuExtension;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;\nimport net.fabricmc.fabric.api.networking.v1.PacketByteBufs;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.inventory.MenuType;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n@Mixin(AbstractContainerMenu.class)\npublic abstract class AbstractContainerMenuMixin implements OwoAbstractContainerMenu, OwoAbstractContainerMenuExtension {\n\n    @Shadow private boolean suppressRemoteUpdates;\n\n    @Unique private final List<SyncedProperty<?>> properties = new ArrayList<>();\n\n    @Unique private final Map<Class<?>, ScreenhandlerMessageData<?>> messages = new LinkedHashMap<>();\n    @Unique private final List<ScreenhandlerMessageData<?>> clientBoundMessages = new ArrayList<>();\n    @Unique private final List<ScreenhandlerMessageData<?>> serverBoundMessages = new ArrayList<>();\n\n    @Unique private Player player = null;\n\n    @Unique\n    private ReflectiveEndecBuilder builder;\n\n    @Inject(method = \"<init>\", at = @At(\"TAIL\"))\n    private void createReflectiveBuilder(MenuType type, int syncId, CallbackInfo ci) {\n        this.builder = MinecraftEndecs.addDefaults(new ReflectiveEndecBuilder());\n    }\n\n    @Override\n    public ReflectiveEndecBuilder endecBuilder() {\n        return builder;\n    }\n\n    @Override\n    public void owo$attachToPlayer(Player player) {\n        this.player = player;\n    }\n\n    @Override\n    public Player player() {\n        return this.player;\n    }\n\n    @Override\n    public <R extends Record> void addServerboundMessage(Class<R> messageClass, Endec<R> endec, Consumer<R> handler) {\n        int id = this.serverBoundMessages.size();\n\n        var messageData = new ScreenhandlerMessageData<>(id, false, endec, handler);\n        this.serverBoundMessages.add(messageData);\n\n        if (this.messages.put(messageClass, messageData) != null) {\n            throw new NetworkException(messageClass + \" is already registered as a message!\");\n        }\n    }\n\n    @Override\n    public <R extends Record> void addClientboundMessage(Class<R> messageClass, Endec<R> endec, Consumer<R> handler) {\n        int id = this.clientBoundMessages.size();\n\n        var messageData = new ScreenhandlerMessageData<>(id, true, endec, handler);\n        this.clientBoundMessages.add(messageData);\n\n        if (this.messages.put(messageClass, messageData) != null) {\n            throw new NetworkException(messageClass + \" is already registered as a message!\");\n        }\n    }\n\n    @Override\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    public <R extends Record> void sendMessage(@NotNull R message) {\n        if (this.player == null) {\n            throw new NetworkException(\"Tried to send a message before player was attached\");\n        }\n\n        ScreenhandlerMessageData messageData = this.messages.get(message.getClass());\n\n        if (messageData == null) {\n            throw new NetworkException(\"Tried to send message of unknown type \" + message.getClass());\n        }\n\n        var ctx = SerializationContext.attributes(RegistriesAttribute.of(this.player.registryAccess()));\n        var buf = PacketByteBufs.create();\n        buf.write(ctx, messageData.endec(), message);\n\n        var packet = new MenuNetworkingInternals.LocalPacket(messageData.id(), buf);\n\n        if (messageData.clientbound()) {\n            if (!(this.player instanceof ServerPlayer serverPlayer)) {\n                throw new NetworkException(\"Tried to send clientbound message on the server\");\n            }\n\n            ServerPlayNetworking.send(serverPlayer, packet);\n        } else {\n            if (!this.player.level().isClientSide()) {\n                throw new NetworkException(\"Tried to send serverbound message on the client\");\n            }\n\n            this.owo$sendToServer(packet);\n        }\n    }\n\n    @Unique\n    @Environment(EnvType.CLIENT)\n    private void owo$sendToServer(CustomPacketPayload payload) {\n        ClientPlayNetworking.send(payload);\n    }\n\n    @Override\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    public void owo$handlePacket(MenuNetworkingInternals.LocalPacket packet, boolean clientbound) {\n        ScreenhandlerMessageData messageData = (clientbound ? this.clientBoundMessages : this.serverBoundMessages).get(packet.packetId());\n        var ctx = SerializationContext.attributes(RegistriesAttribute.of(this.player.registryAccess()));\n\n        messageData.handler().accept(packet.payload().read(ctx, messageData.endec()));\n    }\n\n    @Override\n    public <T> SyncedProperty<T> createProperty(Class<T> clazz, Endec<T> endec, T initial) {\n        var prop = new SyncedProperty<>(this.properties.size(), endec, initial, (AbstractContainerMenu)(Object) this);\n        this.properties.add(prop);\n        return prop;\n    }\n\n    @Override\n    public void owo$readPropertySync(MenuNetworkingInternals.SyncPropertiesPacket packet) {\n        int count = packet.payload().readVarInt();\n\n        for (int i = 0; i < count; i++) {\n            int idx = packet.payload().readVarInt();\n            this.properties.get(idx).read(packet.payload());\n        }\n    }\n\n    @Inject(method = \"sendAllDataToRemote\", at = @At(\"RETURN\"))\n    private void syncOnSyncState(CallbackInfo ci) {\n        this.syncProperties();\n    }\n\n    @Inject(method = \"broadcastChanges\", at = @At(\"RETURN\"))\n    private void syncOnSendContentUpdates(CallbackInfo ci) {\n        if (suppressRemoteUpdates) return;\n\n        this.syncProperties();\n    }\n\n    @Unique\n    private void syncProperties() {\n        if (this.player == null) return;\n        if (!(this.player instanceof ServerPlayer player)) return;\n\n        int count = 0;\n\n        for (var property : this.properties) {\n            if (property.needsSync()) count++;\n        }\n\n        if (count == 0) return;\n\n        var buf = PacketByteBufs.create();\n        buf.writeVarInt(count);\n\n        for (var prop : properties) {\n            if (!prop.needsSync()) continue;\n\n            buf.writeVarInt(prop.index());\n            prop.write(buf);\n        }\n\n        ServerPlayNetworking.send(player, new MenuNetworkingInternals.SyncPropertiesPacket(buf));\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ClientCommonPacketListenerImplAccessor.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl;\nimport net.minecraft.network.Connection;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(ClientCommonPacketListenerImpl.class)\npublic interface ClientCommonPacketListenerImplAccessor {\n    @Accessor\n    Connection getConnection();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ClientConfigurationPacketListenerImplMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.network.OwoClientConnectionExtension;\nimport io.wispforest.owo.network.QueuedChannelSet;\nimport net.minecraft.client.multiplayer.ClientConfigurationPacketListenerImpl;\nimport net.minecraft.network.Connection;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.ModifyArg;\n\n@Mixin(ClientConfigurationPacketListenerImpl.class)\npublic class ClientConfigurationPacketListenerImplMixin {\n\n    @ModifyArg(method = \"handleConfigurationFinished\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/multiplayer/ClientPacketListener;<init>(Lnet/minecraft/client/Minecraft;Lnet/minecraft/network/Connection;Lnet/minecraft/client/multiplayer/CommonListenerCookie;)V\"))\n    private Connection applyChannelSet(Connection connection) {\n        ((OwoClientConnectionExtension) connection).owo$setChannelSet(QueuedChannelSet.channels);\n        QueuedChannelSet.channels = null;\n\n        return connection;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ClientHandshakePacketListenerImplAccessor.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl;\nimport net.minecraft.network.Connection;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(ClientHandshakePacketListenerImpl.class)\npublic interface ClientHandshakePacketListenerImplAccessor {\n\n    @Accessor(\"connection\")\n    Connection owo$getConnection();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ConnectionMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.network.OwoClientConnectionExtension;\nimport net.minecraft.network.Connection;\nimport net.minecraft.resources.Identifier;\nimport org.spongepowered.asm.mixin.Mixin;\n\nimport java.util.Collections;\nimport java.util.Set;\n\n@Mixin(Connection.class)\npublic class ConnectionMixin implements OwoClientConnectionExtension {\n    private Set<Identifier> channels = Collections.emptySet();\n\n    @Override\n    public void owo$setChannelSet(Set<Identifier> channels) {\n        this.channels = channels;\n    }\n\n    @Override\n    public Set<Identifier> owo$getChannelSet() {\n        return this.channels;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/Copenhagen.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.util.Maldenhagen;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.world.level.WorldGenLevel;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.chunk.BulkSectionAccess;\nimport net.minecraft.world.level.chunk.LevelChunkSection;\nimport net.minecraft.world.level.levelgen.feature.OreFeature;\nimport net.minecraft.world.level.levelgen.feature.configurations.OreConfiguration;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\nimport org.spongepowered.asm.mixin.injection.callback.LocalCapture;\n\nimport java.util.BitSet;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\n\n// welcome to maldenhagen, it moved\n// it originally lived in things, but it was malding too hard there\n// see Maldenhagen for how this is used\n@Mixin(OreFeature.class)\npublic class Copenhagen {\n\n    // this map contains the seethe'd orr blocks. its quite important\n    @Unique private final ThreadLocal<Map<BlockPos, BlockState>> COPING = ThreadLocal.withInitial(HashMap::new);\n\n    // this target method is just so damn complex that not even mixin can correctly guess the injector signature.\n    // i just kinda gave up and deleted some of them until it worked. very epic\n    //\n    // oh also the method caches all the spots that gleaming ore was placed at, so we can later update them for it to glow.\n    // of course that needs to be done later, because mojang decided it should. the actual reason is that ChunkSectionCache\n    // locks its chunk sections.\n    //\n    // now you would think this throws an error when you then try to modify those sections. but no.\n    // it just silently deadlocks the entire game\n    @SuppressWarnings(\"InvalidInjectorMethodSignature\")\n    @Inject(method = \"doPlace\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/world/level/chunk/LevelChunkSection;setBlockState(IIILnet/minecraft/world/level/block/state/BlockState;Z)Lnet/minecraft/world/level/block/state/BlockState;\"), locals = LocalCapture.CAPTURE_FAILHARD)\n    private void malding(WorldGenLevel world, RandomSource random, OreConfiguration config, double startX, double endX, double startZ, double endZ,\n                         double startY, double endY, int p_x, int p_y, int p_z, int p_horizontalSize, int p_verticalSize, CallbackInfoReturnable<Boolean> cir,\n                         int i, BitSet bitSet, BlockPos.MutableBlockPos mutable, int j, double[] ds, BulkSectionAccess chunkSectionCache, int m, double d, double e,\n                         double g, double h, int n, int o, int p, int q, int r, int s, int t, double u, int v, double w, int aa, double x, int ab, LevelChunkSection chunkSection,\n                         int ad, int ae, int af, BlockState blockState, Iterator<OreConfiguration.TargetBlockState> var57, OreConfiguration.TargetBlockState target) {\n\n        if (!Maldenhagen.isOnCopium(target.state.getBlock())) return;\n        COPING.get().put(new BlockPos(t, v, aa), target.state);\n    }\n\n    // now in here we read all the gleaming ore spots from our cache and actually cause a block update so that the\n    // lighting calculations happen. all of this just so that some dumb orr block can glow.\n    @Inject(method = \"doPlace\", at = @At(\"TAIL\"))\n    private void coping(WorldGenLevel world, net.minecraft.util.RandomSource random, OreConfiguration config, double startX, double endX,\n                        double startZ, double endZ, double startY, double endY, int x, int y, int z, int horizontalSize,\n                        int verticalSize, CallbackInfoReturnable<Boolean> cir) {\n\n        COPING.get().forEach((blockPos, state) -> {\n            world.setBlock(blockPos, state, Block.UPDATE_ALL);\n        });\n        COPING.get().clear();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/GuiGraphicsMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.ui.util.MatrixStackTransformer;\nimport net.minecraft.client.gui.GuiGraphics;\nimport org.joml.Matrix3x2fStack;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\n@Mixin(GuiGraphics.class)\npublic abstract class GuiGraphicsMixin implements MatrixStackTransformer {\n\n    @Shadow public abstract Matrix3x2fStack pose();\n\n    @Override\n    public Matrix3x2fStack getMatrixStack() {\n        return this.pose();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/MainMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.util.OwoFreezer;\nimport net.minecraft.server.Main;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Group;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(value = Main.class, priority = 0)\npublic class MainMixin {\n\n    @SuppressWarnings({\"MixinAnnotationTarget\"})\n    @Group(name = \"serverFreezeHooks\", min = 1, max = 1)\n    @Inject(method = \"main\", at = @At(value = \"INVOKE\", remap = false,\n            target = \"Lnet/fabricmc/loader/impl/game/minecraft/Hooks;startServer(Ljava/io/File;Ljava/lang/Object;)V\", shift = At.Shift.AFTER))\n    private static void afterFabricHook(CallbackInfo ci) {\n        OwoFreezer.freeze();\n    }\n\n    @SuppressWarnings({\"MixinAnnotationTarget\"})\n    @Group(name = \"serverFreezeHooks\", min = 1, max = 1)\n    @Inject(method = \"main\", at = @At(value = \"INVOKE\", remap = false,\n            target = \"Lorg/quiltmc/loader/impl/game/minecraft/Hooks;startServer(Ljava/io/File;Ljava/lang/Object;)V\", shift = At.Shift.AFTER))\n    private static void afterQuiltHook(CallbackInfo ci) {\n        OwoFreezer.freeze();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/MinecraftMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.util.OwoFreezer;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.main.GameConfig;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Group;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(value = Minecraft.class, priority = 0)\npublic class MinecraftMixin {\n\n    @SuppressWarnings({\"MixinAnnotationTarget\"})\n    @Group(name = \"clientFreezeHooks\", min = 1, max = 1)\n    @Inject(method = \"<init>\", at = @At(value = \"INVOKE\", remap = false,\n            target = \"Lnet/fabricmc/loader/impl/game/minecraft/Hooks;startClient(Ljava/io/File;Ljava/lang/Object;)V\", shift = At.Shift.AFTER))\n    private void afterFabricHook(GameConfig args, CallbackInfo ci) {\n        OwoFreezer.freeze();\n    }\n\n    @SuppressWarnings({\"MixinAnnotationTarget\"})\n    @Group(name = \"clientFreezeHooks\", min = 1, max = 1)\n    @Inject(method = \"<init>\", at = @At(value = \"INVOKE\", remap = false,\n            target = \"Lorg/quiltmc/loader/impl/game/minecraft/Hooks;startClient(Ljava/io/File;Ljava/lang/Object;)V\", shift = At.Shift.AFTER))\n    private void afterQuiltHook(GameConfig args, CallbackInfo ci) {\n        OwoFreezer.freeze();\n    }\n\n}\n\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ServerCommonPacketListenerImplAccessor.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport net.minecraft.network.Connection;\nimport net.minecraft.server.network.ServerCommonPacketListenerImpl;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(ServerCommonPacketListenerImpl.class)\npublic interface ServerCommonPacketListenerImplAccessor {\n\n    @Accessor(\"connection\")\n    Connection owo$getConnection();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ServerPlayerGameModeMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.util.pond.OwoItemExtensions;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.server.level.ServerPlayerGameMode;\nimport net.minecraft.stats.Stats;\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.world.InteractionResult;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.Level;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(ServerPlayerGameMode.class)\npublic class ServerPlayerGameModeMixin {\n\n    @Inject(method = \"useItem\", at = @At(\"RETURN\"))\n    private void incrementUseState(ServerPlayer player, Level world, ItemStack stack, InteractionHand hand, CallbackInfoReturnable<InteractionResult> cir) {\n        var result = cir.getReturnValue();\n\n        if(((OwoItemExtensions) stack.getItem()).owo$shouldTrackUsageStat() || (result instanceof InteractionResult.Success successResult && successResult.wasItemInteraction())) {\n            player.awardStat(Stats.ITEM_USED.get(stack.getItem()));\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ServerPlayerMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.util.pond.OwoAbstractContainerMenuExtension;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(ServerPlayer.class)\npublic class ServerPlayerMixin {\n\n    @SuppressWarnings(\"ConstantConditions\")\n    @Inject(method = \"initMenu\", at = @At(\"HEAD\"))\n    private void attachScreenHandler(AbstractContainerMenu screenHandler, CallbackInfo ci) {\n        ((OwoAbstractContainerMenuExtension) screenHandler).owo$attachToPlayer((ServerPlayer) (Object) this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/SetComponentsFunctionAccessor.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport net.minecraft.core.component.DataComponentPatch;\nimport net.minecraft.world.level.storage.loot.functions.SetComponentsFunction;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemCondition;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\nimport java.util.List;\n\n@Mixin(SetComponentsFunction.class)\npublic interface SetComponentsFunctionAccessor {\n    @Invoker(\"<init>\")\n    static SetComponentsFunction createSetComponentsLootFunction(List<LootItemCondition> list, DataComponentPatch componentChanges) {\n        throw new UnsupportedOperationException();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/TagLoaderMixin.java",
    "content": "package io.wispforest.owo.mixin;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.util.TagInjector;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport net.minecraft.tags.TagLoader;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n@Mixin(TagLoader.class)\npublic class TagLoaderMixin {\n\n    @Shadow\n    @Final\n    private String directory;\n\n    @Inject(method = \"load\", at = @At(\"TAIL\"))\n    public void injectValues(ResourceManager manager, CallbackInfoReturnable<Map<Identifier, List<TagLoader.EntryWithSource>>> cir) {\n        var map = cir.getReturnValue();\n\n        TagInjector.ADDITIONS.forEach((location, entries) -> {\n            if (!this.directory.equals(location.type())) return;\n\n            var list = map.computeIfAbsent(location.tagId(), id -> new ArrayList<>());\n            entries.forEach(addition -> list.add(new TagLoader.EntryWithSource(addition, Owo.MOD_ID)));\n        });\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/ClickableStyleFinderAccessor.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport net.minecraft.client.gui.ActiveTextCollector;\nimport net.minecraft.network.chat.Style;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.function.Consumer;\n\n@Mixin(ActiveTextCollector.ClickableStyleFinder.class)\npublic interface ClickableStyleFinderAccessor {\n    @Mutable\n    @Accessor(\"styleScanner\")\n    void owo$setStyleScanner(Consumer<Style> setStyleCallback);\n\n    @Accessor(\"result\")\n    void owo$setResult(Style style);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/GameRendererAccessor.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport net.minecraft.client.gui.render.GuiRenderer;\nimport net.minecraft.client.renderer.GameRenderer;\nimport net.minecraft.client.renderer.fog.FogRenderer;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(GameRenderer.class)\npublic interface GameRendererAccessor {\n    @Accessor(\"guiRenderer\")\n    GuiRenderer owo$getGuiRenderer();\n\n    @Accessor(\"fogRenderer\")\n    FogRenderer owo$getFogRenderer();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/GuiRendererAccessor.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport net.minecraft.client.gui.render.GuiRenderer;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.GuiRenderState;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.Map;\n\n@Mixin(value = GuiRenderer.class, priority = 1100)\npublic interface GuiRendererAccessor {\n    @Accessor(\"renderState\")\n    GuiRenderState owo$getRenderState();\n\n    @Accessor(\"pictureInPictureRenderers\")\n    Map<Class<? extends PictureInPictureRenderState>, PictureInPictureRenderer<?>> owo$getPictureInPictureRenderers();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/GuiRendererMixin.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport com.llamalad7.mixinextras.injector.ModifyExpressionValue;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.mojang.blaze3d.buffers.GpuBufferSlice;\nimport com.mojang.blaze3d.pipeline.RenderTarget;\nimport com.mojang.blaze3d.systems.RenderSystem;\nimport com.mojang.blaze3d.textures.AddressMode;\nimport com.mojang.blaze3d.textures.FilterMode;\nimport com.mojang.blaze3d.textures.GpuSampler;\nimport io.wispforest.owo.braid.core.BraidRenderPipelines;\nimport io.wispforest.owo.braid.util.BraidGuiRenderer;\nimport io.wispforest.owo.util.pond.BraidGuiRendererExtension;\nimport net.minecraft.client.gui.render.GuiRenderer;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.CachedOrthoProjectionMatrixBuffer;\nimport org.jspecify.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.ModifyArg;\n\nimport java.util.Map;\n\n@Mixin(GuiRenderer.class)\npublic class GuiRendererMixin implements BraidGuiRendererExtension {\n\n    @Unique\n    private BraidGuiRenderer.Target target = null;\n\n    @Override\n    public void owo$setTarget(BraidGuiRenderer.Target target) {\n        this.target = target;\n    }\n\n    // ---\n\n    @WrapOperation(method = \"draw\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/renderer/CachedOrthoProjectionMatrixBuffer;getBuffer(FF)Lcom/mojang/blaze3d/buffers/GpuBufferSlice;\"))\n    private GpuBufferSlice injectSurfaceDimensions(CachedOrthoProjectionMatrixBuffer instance, float width, float height, Operation<GpuBufferSlice> original) {\n        if (this.target == null) return original.call(instance, width, height);\n\n        var surface = this.target.surface();\n        return original.call(instance, (float) surface.width(), (float) surface.height());\n    }\n\n    @ModifyExpressionValue(method = \"draw\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/Minecraft;getMainRenderTarget()Lcom/mojang/blaze3d/pipeline/RenderTarget;\"))\n    private RenderTarget injectFramebuffer(RenderTarget original) {\n        if (this.target == null) return original;\n        return this.target.framebuffer();\n    }\n\n    @ModifyExpressionValue(method = \"enableScissor\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/platform/Window;getHeight()I\"))\n    private int injectSurfaceHeightForScissor(int original) {\n        if (this.target == null) return original;\n        return this.target.framebuffer().height;\n    }\n\n    @ModifyExpressionValue(method = \"enableScissor\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/platform/Window;getGuiScale()I\"))\n    private int injectSurfaceScaleForScissor(int original) {\n        if (this.target == null) return original;\n        return (int) this.target.surface().scaleFactor();\n    }\n\n    @ModifyExpressionValue(method = \"preparePictureInPicture\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/platform/Window;getGuiScale()I\"))\n    private int injectSurfaceScaleForPIP(int original) {\n        if (this.target == null) return original;\n        return (int) this.target.surface().scaleFactor();\n    }\n\n    @ModifyExpressionValue(method = \"getGuiScaleInvalidatingItemAtlasIfChanged\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/platform/Window;getGuiScale()I\"))\n    private int injectSurfaceScaleForItemAtlas(int original) {\n        if (this.target == null) return original;\n        return (int) this.target.surface().scaleFactor();\n    }\n\n    // ---\n\n    @ModifyArg(\n        method = \"executeDraw(Lnet/minecraft/client/gui/render/GuiRenderer$Draw;Lcom/mojang/blaze3d/systems/RenderPass;Lcom/mojang/blaze3d/buffers/GpuBuffer;Lcom/mojang/blaze3d/vertex/VertexFormat$IndexType;)V\",\n        at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/systems/RenderPass;bindTexture(Ljava/lang/String;Lcom/mojang/blaze3d/textures/GpuTextureView;Lcom/mojang/blaze3d/textures/GpuSampler;)V\", ordinal = 0),\n        index = 2\n    )\n    private @Nullable GpuSampler injectTextureFilter(GpuSampler sampler, @Local(argsOnly = true) GuiRenderer.Draw draw) {\n        if (draw.textureSetup().texure0() == null) {\n            return sampler;\n        }\n\n        if (draw.pipeline() == BraidRenderPipelines.TEXTURED_BILINEAR) {\n            return RenderSystem.getSamplerCache().getSampler(AddressMode.REPEAT, AddressMode.REPEAT, FilterMode.LINEAR, FilterMode.LINEAR, false);\n        } else if (draw.pipeline() == BraidRenderPipelines.TEXTURED_NEAREST) {\n            return RenderSystem.getSamplerCache().getSampler(AddressMode.REPEAT, AddressMode.REPEAT, FilterMode.NEAREST, FilterMode.NEAREST, false);\n        } else {\n            return sampler;\n        }\n    }\n\n    // ---\n\n    @ModifyExpressionValue(method = \"close\", at = @At(value = \"FIELD\", target = \"Lnet/minecraft/client/gui/render/GuiRenderer;pictureInPictureRenderers:Ljava/util/Map;\"))\n    private Map<Class<? extends PictureInPictureRenderState>, PictureInPictureRenderer<?>> keepAliveRenderers(Map<Class<? extends PictureInPictureRenderState>, PictureInPictureRenderer<?>> original) {\n        if (((Object) this) instanceof BraidGuiRenderer) {\n            return Map.of();\n        }\n\n        return original;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/KeyboardHandlerMixin.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport io.wispforest.owo.braid.core.events.CharInputEvent;\nimport io.wispforest.owo.braid.util.layers.BraidLayersBinding;\nimport net.minecraft.client.KeyboardHandler;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.input.CharacterEvent;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\n@Mixin(KeyboardHandler.class)\npublic class KeyboardHandlerMixin {\n\n    @WrapOperation(method = \"charTyped\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/screens/Screen;charTyped(Lnet/minecraft/client/input/CharacterEvent;)Z\"))\n    private boolean captureScreenCharTyped(Screen screen, CharacterEvent charInput, Operation<Boolean> original) {\n        return BraidLayersBinding.tryHandleEvent(screen, new CharInputEvent((char) charInput.codepoint(), charInput.modifiers()))\n            || original.call(screen, charInput);\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/LevelRendererMixin.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.mojang.blaze3d.buffers.GpuBufferSlice;\nimport com.mojang.blaze3d.resource.ResourceHandle;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport net.minecraft.client.renderer.LevelRenderer;\nimport net.minecraft.client.renderer.SubmitNodeStorage;\nimport net.minecraft.client.renderer.state.LevelRenderState;\nimport net.minecraft.util.profiling.ProfilerFiller;\nimport org.joml.Matrix4f;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(LevelRenderer.class)\npublic class LevelRendererMixin {\n\n    @Shadow\n    @Final\n    private SubmitNodeStorage submitNodeStorage;\n\n    @Inject(method = \"method_62214\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/renderer/LevelRenderer;submitBlockEntities(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/state/LevelRenderState;Lnet/minecraft/client/renderer/SubmitNodeStorage;)V\", shift = At.Shift.AFTER))\n    private void renderBraidDisplays(GpuBufferSlice gpuBufferSlice, LevelRenderState levelRenderState, ProfilerFiller profiler, Matrix4f matrix4f, ResourceHandle<?> handle, ResourceHandle<?> handle2, boolean bl, ResourceHandle<?> handle3, ResourceHandle<?> handle4, CallbackInfo ci, @Local PoseStack matrixStack) {\n        BraidDisplayBinding.renderAutomaticDisplays(matrixStack, levelRenderState.cameraRenderState, submitNodeStorage);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/Matrix3x2fStackAccessor.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport org.joml.Matrix3x2f;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@org.spongepowered.asm.mixin.Mixin(org.joml.Matrix3x2fStack.class)\npublic interface Matrix3x2fStackAccessor {\n    @Accessor(\"mats\")\n    Matrix3x2f[] owo$getMats();\n\n    @Accessor(\"mats\")\n    void owo$setMats(Matrix3x2f[] mats);\n\n    @Accessor(\"curr\")\n    int owo$getCurr();\n\n    @Accessor(\"curr\")\n    void owo$setCurr(int curr);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/RenderTypeInvoker.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport net.minecraft.client.renderer.rendertype.RenderSetup;\nimport net.minecraft.client.renderer.rendertype.RenderType;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@org.spongepowered.asm.mixin.Mixin(net.minecraft.client.renderer.rendertype.RenderType.class)\npublic interface RenderTypeInvoker {\n    @Invoker(\"create\")\n    static RenderType owo$of(String name, RenderSetup renderSetup) {throw new UnsupportedOperationException();}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/ScreenMixin.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport io.wispforest.owo.braid.util.layers.BraidLayersBinding;\nimport io.wispforest.owo.util.pond.OwoScreenExtension;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.Screen;\nimport org.jetbrains.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(value = Screen.class, priority = 1100)\npublic abstract class ScreenMixin implements OwoScreenExtension {\n\n    @Unique\n    private @Nullable BraidLayersBinding.LayersState braidLayersState;\n\n    @Override\n    public void owo$setBraidLayersState(BraidLayersBinding.LayersState state) {\n        this.braidLayersState = state;\n    }\n\n    @Override\n    public @Nullable BraidLayersBinding.LayersState owo$getBraidLayersState() {\n        return this.braidLayersState;\n    }\n\n    @Inject(method = \"renderWithTooltipAndSubtitles\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/screens/Screen;render(Lnet/minecraft/client/gui/GuiGraphics;IIF)V\", shift = At.Shift.AFTER))\n    private void renderLayers(GuiGraphics context, int mouseX, int mouseY, float deltaTicks, CallbackInfo ci) {\n        BraidLayersBinding.renderLayers(((Screen) (Object) this), context, mouseX, mouseY);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/braid/ToastManagerMixin.java",
    "content": "package io.wispforest.owo.mixin.braid;\n\nimport io.wispforest.owo.braid.util.BraidToast;\nimport net.minecraft.client.gui.components.toasts.ToastManager;\nimport org.apache.commons.lang3.mutable.MutableBoolean;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\nimport java.util.List;\n\n@Mixin(ToastManager.class)\npublic class ToastManagerMixin {\n\n    @Shadow\n    @Final\n    private List<ToastManager.ToastInstance<?>> visibleToasts;\n\n    @Inject(method = \"method_61991\", at = @At(value = \"INVOKE\", target = \"Ljava/util/BitSet;clear(II)V\"))\n    private void disposeBraidToasts(MutableBoolean mutableBoolean, ToastManager.ToastInstance<?> entry, CallbackInfoReturnable<Boolean> cir) {\n        if (entry.getToast() instanceof BraidToast toast) {\n            toast.dispose();\n        }\n    }\n\n    @Inject(method = \"clear\", at = @At(\"HEAD\"))\n    private void disposeBraidToastsEpisode2(CallbackInfo ci) {\n        for (var entry : this.visibleToasts) {\n            if (entry.getToast() instanceof BraidToast toast) {\n                toast.dispose();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ext/ItemMixin.java",
    "content": "package io.wispforest.owo.mixin.ext;\n\nimport io.wispforest.owo.ext.OwoItem;\nimport net.minecraft.world.item.Item;\nimport org.spongepowered.asm.mixin.Mixin;\n\n@Mixin(Item.class)\npublic class ItemMixin implements OwoItem {\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ext/ItemStackMixin.java",
    "content": "package io.wispforest.owo.mixin.ext;\n\nimport io.wispforest.owo.ext.DerivedComponentMap;\nimport net.minecraft.core.component.DataComponentMap;\nimport net.minecraft.core.component.DataComponentPatch;\nimport net.minecraft.core.component.PatchedDataComponentMap;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.ItemLike;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(ItemStack.class)\npublic class ItemStackMixin {\n    @Shadow @Final\n    PatchedDataComponentMap components;\n\n    @Unique private DerivedComponentMap derivedMap;\n\n    @Inject(method = \"<init>(Lnet/minecraft/world/level/ItemLike;ILnet/minecraft/core/component/PatchedDataComponentMap;)V\", at = @At(\"TAIL\"))\n    private void injectDerivedComponentMap(ItemLike item, int count, PatchedDataComponentMap components, CallbackInfo ci) {\n        var base = ((PatchedDataComponentMapAccessor)(Object) this.components).owo$getPrototype();\n\n        if (base instanceof DerivedComponentMap derived) {\n            derivedMap = derived;\n        } else {\n            derivedMap = new DerivedComponentMap(base);\n            ((PatchedDataComponentMapAccessor)(Object) this.components).owo$setPrototype(derivedMap);\n        }\n    }\n\n    @Inject(method = \"applyComponentsAndValidate\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/core/component/PatchedDataComponentMap;applyPatch(Lnet/minecraft/core/component/DataComponentPatch;)V\", shift = At.Shift.AFTER))\n    private void deriveComponents2(DataComponentPatch changes, CallbackInfo ci) {\n        if (derivedMap == null) return;\n        derivedMap.derive((ItemStack)(Object) this);\n    }\n\n    @Inject(method = \"applyComponents(Lnet/minecraft/core/component/DataComponentPatch;)V\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/core/component/PatchedDataComponentMap;applyPatch(Lnet/minecraft/core/component/DataComponentPatch;)V\", shift = At.Shift.AFTER))\n    private void deriveComponents3(DataComponentPatch changes, CallbackInfo ci) {\n        if (derivedMap == null) return;\n        derivedMap.derive((ItemStack)(Object) this);\n    }\n\n    @Inject(method = \"applyComponents(Lnet/minecraft/core/component/DataComponentMap;)V\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/core/component/PatchedDataComponentMap;setAll(Lnet/minecraft/core/component/DataComponentMap;)V\", shift = At.Shift.AFTER))\n    private void deriveComponents4(DataComponentMap components, CallbackInfo ci) {\n        if (derivedMap == null) return;\n        derivedMap.derive((ItemStack)(Object) this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ext/PatchedDataComponentMapAccessor.java",
    "content": "package io.wispforest.owo.mixin.ext;\n\nimport net.minecraft.core.component.DataComponentMap;\nimport net.minecraft.core.component.PatchedDataComponentMap;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(PatchedDataComponentMap.class)\npublic interface PatchedDataComponentMapAccessor {\n    @Accessor(\"prototype\")\n    DataComponentMap owo$getPrototype();\n\n    @Accessor(\"prototype\")\n    @Mutable\n    void owo$setPrototype(DataComponentMap baseComponents);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ext/PatchedDataComponentMapMixin.java",
    "content": "package io.wispforest.owo.mixin.ext;\n\nimport com.llamalad7.mixinextras.injector.ModifyExpressionValue;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport io.wispforest.owo.ext.DerivedComponentMap;\nimport net.minecraft.core.component.DataComponentMap;\nimport net.minecraft.core.component.PatchedDataComponentMap;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\n@Mixin(PatchedDataComponentMap.class)\npublic class PatchedDataComponentMapMixin {\n    @ModifyExpressionValue(method = \"copy\", at = @At(value = \"FIELD\", target = \"Lnet/minecraft/core/component/PatchedDataComponentMap;prototype:Lnet/minecraft/core/component/DataComponentMap;\"))\n    private DataComponentMap reWrapDerived(DataComponentMap original) {\n        return DerivedComponentMap.reWrapIfNeeded(original);\n    }\n\n    @WrapOperation(method = \"equals\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/core/component/DataComponentMap;equals(Ljava/lang/Object;)Z\"))\n    private boolean prioritiseDerivedMap(DataComponentMap instance, Object object, Operation<Boolean> original) {\n        return (object instanceof DerivedComponentMap derivedComponentMap)\n            ? original.call(derivedComponentMap, instance)\n            : original.call(instance, object);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/extension/SimpleJsonResourceReloadListenerMixin.java",
    "content": "package io.wispforest.owo.mixin.extension;\n\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParseException;\nimport com.google.gson.JsonPrimitive;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.mojang.serialization.JsonOps;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.extension.recipe.RecipeManagerAccessor;\nimport io.wispforest.owo.util.RecipeRemainderStorage;\nimport net.minecraft.resources.FileToIdConverter;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;\nimport net.minecraft.util.GsonHelper;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\nimport java.io.Reader;\nimport java.util.HashMap;\n\n@Mixin(SimpleJsonResourceReloadListener.class)\npublic abstract class SimpleJsonResourceReloadListenerMixin {\n\n    @WrapOperation(\n        method = \"scanDirectory(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/resources/FileToIdConverter;Lcom/mojang/serialization/DynamicOps;Lcom/mojang/serialization/Codec;Ljava/util/Map;)V\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/util/StrictJsonParser;parse(Ljava/io/Reader;)Lcom/google/gson/JsonElement;\"\n        )\n    )\n    private static JsonElement loadRecipeExtensions(\n        Reader jsonReader,\n        Operation<JsonElement> original,\n        @Local(argsOnly = true) FileToIdConverter finder,\n        @Local(ordinal = 1) Identifier recipeId\n    ) {\n        var element = original.call(jsonReader);\n\n        if (RecipeManagerAccessor.owo$getFinder() == finder && element instanceof JsonObject json) {\n            if (json.has(Owo.id(\"remainders\").toString())) {\n                var remainders = new HashMap<Item, ItemStack>();\n\n                for (var remainderEntry : json.getAsJsonObject(Owo.id(\"remainders\").toString()).entrySet()) {\n                    var item = GsonHelper.convertToItem(new JsonPrimitive(remainderEntry.getKey()), remainderEntry.getKey());\n\n                    if (remainderEntry.getValue().isJsonObject()) {\n                        var remainderStack = ItemStack.CODEC.parse(\n                            JsonOps.INSTANCE,\n                            remainderEntry.getValue().getAsJsonObject()\n                        ).getOrThrow(JsonParseException::new);\n                        remainders.put(item.value(), remainderStack);\n                    } else {\n                        var remainderItem = GsonHelper.convertToItem(remainderEntry.getValue(), \"item\");\n                        remainders.put(item.value(), new ItemStack(remainderItem));\n                    }\n                }\n\n                if (!remainders.isEmpty()) RecipeRemainderStorage.store(recipeId, remainders);\n            }\n        }\n\n        return element;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/extension/json5/FallbackResourceManagerMixin.java",
    "content": "package io.wispforest.owo.mixin.extension.json5;\n\nimport com.llamalad7.mixinextras.injector.v2.WrapWithCondition;\nimport com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport io.wispforest.owo.util.DataExtensionUtil;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.packs.PackResources;\nimport net.minecraft.server.packs.PackType;\nimport net.minecraft.server.packs.resources.FallbackResourceManager;\nimport net.minecraft.server.packs.resources.Resource;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\n\nimport static io.wispforest.owo.util.DataExtensionUtil.coerceJson;\n\n@Mixin(FallbackResourceManager.class)\npublic abstract class FallbackResourceManagerMixin {\n\n    @WrapMethod(method = \"getResourceStack\")\n    private List<Resource> json5$getAllResources(Identifier id, Operation<List<Resource>> original) {\n        var base = original.call(id);\n        if (id.getPath().endsWith(\".json\")) original\n            .call(id.withPath(id.getPath() + 5))\n            .forEach(resource -> {\n                if (DataExtensionUtil.JSON5_ENABLED_PACKS.contains(resource.source().packId())) {\n                    base.add(new Resource(resource.source(), () -> coerceJson(resource.open())));\n                }\n            });\n        return base;\n    }\n\n    @WrapWithCondition(\n        method = \"listResources\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/server/packs/PackResources;listResources(Lnet/minecraft/server/packs/PackType;Ljava/lang/String;Ljava/lang/String;Lnet/minecraft/server/packs/PackResources$ResourceOutput;)V\"\n        )\n    )\n    private boolean json5$findResources(\n        PackResources instance,\n        PackType resourceType,\n        String namespace,\n        String startingPath,\n        PackResources.ResourceOutput resultConsumer,\n        @Local(argsOnly = true) Predicate<Identifier> predicate\n    ) {\n        return !(predicate instanceof DataExtensionUtil.OptInIdentifierPredicate)\n               || DataExtensionUtil.JSON5_ENABLED_PACKS.contains(instance.packId());\n    }\n\n    @WrapWithCondition(\n        method = \"listResourceStacks\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/server/packs/resources/FallbackResourceManager;listPackResources(Lnet/minecraft/server/packs/resources/FallbackResourceManager$PackEntry;Ljava/lang/String;Ljava/util/function/Predicate;Ljava/util/Map;)V\"\n        )\n    )\n    private boolean json5$findAllResources(\n        FallbackResourceManager instance,\n        FallbackResourceManager.PackEntry pack,\n        String startingPath,\n        Predicate<Identifier> allowedPathPredicate,\n        Map<?, ?> idToEntryList,\n        @Local(argsOnly = true) Predicate<Identifier> predicate\n    ) {\n        return !(predicate instanceof DataExtensionUtil.OptInIdentifierPredicate)\n               || pack.resources != null && DataExtensionUtil.JSON5_ENABLED_PACKS.contains(pack.resources.packId());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/extension/json5/FileToIdConverterMixin.java",
    "content": "package io.wispforest.owo.mixin.extension.json5;\n\nimport com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport net.minecraft.resources.FileToIdConverter;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.packs.resources.Resource;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\n\nimport static io.wispforest.owo.util.DataExtensionUtil.OptInIdentifierPredicate;\nimport static io.wispforest.owo.util.DataExtensionUtil.coerceJson;\n\n@Mixin(FileToIdConverter.class)\npublic abstract class FileToIdConverterMixin {\n\n    @Shadow @Final private String extension;\n\n    @WrapOperation(\n        method = \"listMatchingResources\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/server/packs/resources/ResourceManager;listResources(Ljava/lang/String;Ljava/util/function/Predicate;)Ljava/util/Map;\"\n        )\n    )\n    private Map<Identifier, Resource> json5$findResources(\n        ResourceManager instance,\n        String directoryName,\n        Predicate<Identifier> identifierPredicate,\n        Operation<Map<Identifier, Resource>> original\n    ) {\n        var base = original.call(instance, directoryName, identifierPredicate);\n        if (this.extension.equals(\".json\")) {\n            original\n                .call(instance, directoryName, OptInIdentifierPredicate.of(path -> path.getPath().endsWith(\".json5\")))\n                .forEach((identifier, resource) -> base.put(\n                    identifier,\n                    new Resource(resource.source(), () -> coerceJson(resource.open()))\n                ));\n        }\n        return base;\n    }\n\n    @WrapOperation(\n        method = \"listMatchingResourceStacks\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/server/packs/resources/ResourceManager;listResourceStacks(Ljava/lang/String;Ljava/util/function/Predicate;)Ljava/util/Map;\"\n        )\n    )\n    private Map<Identifier, List<Resource>> json5$findAllResources(\n        ResourceManager instance,\n        String directoryName,\n        Predicate<Identifier> identifierPredicate,\n        Operation<Map<Identifier, List<Resource>>> original\n    ) {\n        var base = original.call(instance, directoryName, identifierPredicate);\n        if (this.extension.equals(\".json\")) {\n            original\n                .call(instance, directoryName, OptInIdentifierPredicate.of(path -> path.getPath().endsWith(\".json5\")))\n                .forEach((identifier, resources) -> base\n                    .computeIfAbsent(identifier, id -> new ArrayList<>())\n                    .addAll(resources\n                        .stream()\n                        .map(resource -> new Resource(resource.source(), () -> coerceJson(resource.open())))\n                        .toList()\n                    )\n                );\n        }\n        return base;\n    }\n\n    @WrapMethod(method = \"fileToId\")\n    private Identifier json5$fixToResourceId(\n        Identifier path, Operation<Identifier> original\n    ) {\n        if (this.extension.equals(\".json\") && path.getPath().endsWith(\".json5\"))\n            path = path.withPath(path.getPath().substring(0, path.getPath().length() - 1));\n        return original.call(path);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/extension/json5/LanguageReaderMixin.java",
    "content": "package io.wispforest.owo.mixin.extension.json5;\n\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport io.wispforest.owo.util.DataExtensionUtil;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.packs.resources.Resource;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport xyz.nucleoid.server.translations.impl.language.LanguageReader;\n\nimport java.util.Map;\nimport java.util.function.Predicate;\n\nimport static io.wispforest.owo.util.DataExtensionUtil.coerceJson;\n\n@Mixin(LanguageReader.class)\npublic abstract class LanguageReaderMixin {\n\n    @WrapOperation(\n        method = \"collectDataPackTranslations\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/server/packs/resources/ResourceManager;listResources(Ljava/lang/String;Ljava/util/function/Predicate;)Ljava/util/Map;\"\n        )\n    )\n    private static Map<Identifier, Resource> json5$collectDataPackTranslations(\n        ResourceManager instance,\n        String s,\n        Predicate<Identifier> identifierPredicate,\n        Operation<Map<Identifier, Resource>> original\n    ) {\n        var base = original.call(instance, s, identifierPredicate);\n        original.call(instance, s, DataExtensionUtil.OptInIdentifierPredicate.of(path -> path.getPath().endsWith(\".json5\")))\n            .forEach((identifier, resource) -> base.putIfAbsent(\n                identifier, new Resource(resource.source(), () -> coerceJson(resource.open()))\n            ));\n        return base;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/extension/json5/MultiPackResourceManagerMixin.java",
    "content": "package io.wispforest.owo.mixin.extension.json5;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.util.DataExtensionUtil;\nimport net.minecraft.server.packs.PackResources;\nimport net.minecraft.server.packs.PackType;\nimport net.minecraft.server.packs.resources.MultiPackResourceManager;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.List;\n\n@Mixin(MultiPackResourceManager.class)\npublic abstract class MultiPackResourceManagerMixin {\n\n    @Inject(\n        method = \"<init>\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/server/packs/resources/MultiPackResourceManager;getPackFilterSection(Lnet/minecraft/server/packs/PackResources;)Lnet/minecraft/server/packs/resources/ResourceFilterSection;\"\n        )\n    )\n    private static void json5$optInPacks(PackType type, List<PackResources> packs, CallbackInfo ci) {\n        for (var pack : packs) {\n            var inputSupplier = pack.getRootResource(Owo.MOD_ID + \"-json5\");\n            if (inputSupplier != null) {\n                DataExtensionUtil.JSON5_ENABLED_PACKS.add(pack.packId());\n            } else {\n                DataExtensionUtil.JSON5_ENABLED_PACKS.remove(pack.packId());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/extension/recipe/RecipeManagerAccessor.java",
    "content": "package io.wispforest.owo.mixin.extension.recipe;\n\nimport net.minecraft.resources.FileToIdConverter;\nimport net.minecraft.world.item.crafting.RecipeManager;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(RecipeManager.class)\npublic interface RecipeManagerAccessor {\n\n    @Accessor(\"RECIPE_LISTER\")\n    static FileToIdConverter owo$getFinder() {\n        throw new UnsupportedOperationException();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/extension/recipe/ResultSlotMixin.java",
    "content": "package io.wispforest.owo.mixin.extension.recipe;\n\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.llamalad7.mixinextras.sugar.Share;\nimport com.llamalad7.mixinextras.sugar.ref.LocalRef;\nimport io.wispforest.owo.util.RecipeRemainderStorage;\nimport net.minecraft.core.NonNullList;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.inventory.ResultSlot;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.crafting.*;\nimport net.minecraft.world.level.Level;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.Optional;\nimport java.util.function.Function;\n\n@Mixin(ResultSlot.class)\npublic abstract class ResultSlotMixin {\n\n    @Shadow\n    @Final\n    private Player player;\n\n    @Inject(\n        method = \"onTake\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/world/inventory/CraftingContainer;setItem(ILnet/minecraft/world/item/ItemStack;)V\",\n            ordinal = 1\n        )\n    )\n    private void fixRemainderStacking(\n        Player player,\n        ItemStack stack,\n        CallbackInfo ci,\n        @Local(ordinal = 2) ItemStack remainderStack\n    ) {\n        if (remainderStack.getCount() > remainderStack.getMaxStackSize()) {\n            int excess = remainderStack.getCount() - remainderStack.getMaxStackSize();\n            remainderStack.shrink(excess);\n\n            this.player.getInventory().placeItemBackInInventory(remainderStack.copyWithCount(excess));\n        }\n    }\n\n    @WrapOperation(\n        method = \"getRemainingItems\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/world/item/crafting/RecipeManager;getRecipeFor(Lnet/minecraft/world/item/crafting/RecipeType;Lnet/minecraft/world/item/crafting/RecipeInput;Lnet/minecraft/world/level/Level;)Ljava/util/Optional;\"\n        )\n    )\n    private <I extends RecipeInput, T extends Recipe<I>> Optional<RecipeHolder<T>> captureRecipeEntry(\n        RecipeManager instance,\n        RecipeType<T> type,\n        I input,\n        Level world,\n        Operation<Optional<RecipeHolder<T>>> original,\n        @Share(value = \"recipe_entry\") LocalRef<Optional<RecipeHolder<T>>> recipeEntry\n    ) {\n        var entry = original.call(instance, type, input, world);\n\n        recipeEntry.set(entry);\n\n        return entry;\n    }\n\n    @WrapOperation(\n        method = \"getRemainingItems\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Ljava/util/Optional;map(Ljava/util/function/Function;)Ljava/util/Optional;\"\n        )\n    )\n    private <I extends RecipeInput, T extends Recipe<I>> Optional<NonNullList<ItemStack>> addRecipeSpecificRemainders(\n        Optional<T> instance,\n        Function<? super T, ? extends NonNullList<ItemStack>> mapper,\n        Operation<Optional<NonNullList<ItemStack>>> original,\n        @Share(value = \"recipe_entry\") LocalRef<Optional<RecipeHolder<?>>> recipeEntry,\n        @Local(argsOnly = true) CraftingInput input\n    ) {\n        var recipeEntryOptional = recipeEntry.get();\n\n        return original.call(instance, mapper)\n            .map(remainders -> {\n                var recipeId = recipeEntryOptional.get().id().identifier();\n\n                if (RecipeRemainderStorage.has(recipeId)) {\n                    var owoRemainders = RecipeRemainderStorage.get(recipeId);\n\n                    for (int i = 0; i < remainders.size(); ++i) {\n                        var item = input.getItem(i).getItem();\n                        if (!owoRemainders.containsKey(item)) continue;\n\n                        remainders.set(i, owoRemainders.get(item).copy());\n                    }\n                }\n\n                return remainders;\n            });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/CreativeModeInventoryScreenAccessor.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;\nimport net.minecraft.world.item.CreativeModeTab;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(CreativeModeInventoryScreen.class)\npublic interface CreativeModeInventoryScreenAccessor {\n\n    @Accessor(\"selectedTab\")\n    static CreativeModeTab owo$getSelectedTab() {\n        throw new IllegalStateException(\"Mixin stub must not be called\");\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/CreativeModeInventoryScreenMixin.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.mojang.blaze3d.platform.InputConstants;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupButtonWidget;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.util.CursorAdapter;\nimport io.wispforest.owo.util.pond.OwoCreativeInventoryScreenExtensions;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;\nimport net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;\nimport net.minecraft.client.player.LocalPlayer;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.flag.FeatureFlagSet;\nimport net.minecraft.world.item.CreativeModeTab;\nimport org.lwjgl.glfw.GLFW;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.ModifyArg;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n@Mixin(CreativeModeInventoryScreen.class)\npublic abstract class CreativeModeInventoryScreenMixin extends AbstractContainerScreen<CreativeModeInventoryScreen.ItemPickerMenu> implements OwoCreativeInventoryScreenExtensions {\n\n    @Shadow\n    private static CreativeModeTab selectedTab;\n\n    @Shadow\n    protected abstract void init();\n\n    @Shadow\n    protected abstract boolean hasPermissions(Player player);\n\n    @Shadow\n    protected abstract boolean canScroll();\n\n    @Unique\n    private final List<ItemGroupButtonWidget> owoButtons = new ArrayList<>();\n\n    @Unique\n    private FeatureFlagSet enabledFeatures = null;\n\n    @Unique\n    private final CursorAdapter cursorAdapter = CursorAdapter.ofClientWindow();\n\n    @Inject(method = \"<init>\", at = @At(\"TAIL\"))\n    private void captureFeatures(LocalPlayer player, FeatureFlagSet enabledFeatures, boolean operatorTabEnabled, CallbackInfo ci) {\n        this.enabledFeatures = enabledFeatures;\n    }\n\n    // ----------\n    // Background\n    // ----------\n\n    @ModifyArg(method = \"renderBg\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/GuiGraphics;blit(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIFFIIII)V\", ordinal = 0))\n    private Identifier injectCustomGroupTexture(Identifier original) {\n        if (!(selectedTab instanceof OwoItemGroup owoGroup) || owoGroup.getOwoBackgroundTexture() == null) return original;\n        return owoGroup.getOwoBackgroundTexture();\n    }\n\n    // ----------------\n    // Scrollbar slider\n    // ----------------\n\n    @ModifyArg(method = \"renderBg\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/GuiGraphics;blitSprite(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIII)V\"))\n    private Identifier injectCustomScrollbarTexture(Identifier texture) {\n        if (!(selectedTab instanceof OwoItemGroup owoGroup) || owoGroup.getScrollerTextures() == null) return texture;\n\n        return this.canScroll()\n            ? owoGroup.getScrollerTextures().enabled()\n            : owoGroup.getScrollerTextures().disabled();\n    }\n\n    // -------------\n    // Group headers\n    // -------------\n\n    @ModifyArg(method = \"renderTabButton\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/GuiGraphics;blitSprite(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIII)V\"))\n    private Identifier injectCustomTabTexture(Identifier texture, @Local(argsOnly = true) CreativeModeTab group) {\n        if (!(group instanceof OwoItemGroup contextGroup) || contextGroup.getTabTextures() == null) return texture;\n\n        var textures = contextGroup.getTabTextures();\n        return contextGroup.row() == CreativeModeTab.Row.TOP\n            ? selectedTab == contextGroup ? contextGroup.column() == 0 ? textures.topSelectedFirstColumn() : textures.topSelected() : textures.topUnselected()\n            : selectedTab == contextGroup ? contextGroup.column() == 0 ? textures.bottomSelectedFirstColumn() : textures.bottomSelected() : textures.bottomUnselected();\n    }\n\n    @Inject(method = \"renderTabButton\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/world/item/CreativeModeTab;getIconItem()Lnet/minecraft/world/item/ItemStack;\"))\n    private void renderOwoIcon(GuiGraphics context, int mouseX, int mouseY, CreativeModeTab group, CallbackInfo ci, @Local(ordinal = 3) int j, @Local(ordinal = 4) int k) {\n        if (!(group instanceof OwoItemGroup owoGroup)) return;\n\n        owoGroup.icon().render(context, j + 5, k + 7, 0, 0, 0);\n    }\n\n    // -------------\n    // oωo tab title\n    // -------------\n\n    @ModifyArg(method = \"renderLabels\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/GuiGraphics;drawString(Lnet/minecraft/client/gui/Font;Lnet/minecraft/network/chat/Component;IIIZ)V\"))\n    private Component injectTabNameAsTitle(Component original) {\n        if (!(selectedTab instanceof OwoItemGroup owoGroup) || !owoGroup.hasDynamicTitle() || owoGroup.selectedTabs().size() != 1) {\n            return original;\n        }\n\n        var singleActiveTab = owoGroup.getTab(owoGroup.selectedTabs().iterator().nextInt());\n        if (singleActiveTab.primary()) {\n            return singleActiveTab.name();\n        } else {\n            return Component.translatable(\n                \"text.owo.itemGroup.tab_template\",\n                owoGroup.getDisplayName(),\n                singleActiveTab.name()\n            );\n        }\n    }\n\n    // ---------------\n    // oωo tab buttons\n    // ---------------\n\n    @Inject(at = @At(\"HEAD\"), method = \"selectTab(Lnet/minecraft/world/item/CreativeModeTab;)V\")\n    private void setSelectedTab(CreativeModeTab group, CallbackInfo ci) {\n        this.owoButtons.forEach(this::removeWidget);\n        this.owoButtons.clear();\n\n        if (group instanceof OwoItemGroup owoGroup) {\n            int tabRootY = this.topPos;\n\n            final var tabStackHeight = owoGroup.getTabStackHeight();\n            tabRootY -= 13 * (tabStackHeight - 4);\n\n            if (owoGroup.shouldDisplaySingleTab() || owoGroup.tabs.size() > 1) {\n                for (int tabIdx = 0; tabIdx < owoGroup.tabs.size(); tabIdx++) {\n                    var tab = owoGroup.tabs.get(tabIdx);\n\n                    int xOffset = this.leftPos - 27 - (tabIdx / tabStackHeight) * 26;\n                    int yOffset = tabRootY + 10 + (tabIdx % tabStackHeight) * 30;\n\n                    var tabButton = new ItemGroupButtonWidget(xOffset, yOffset, 32, tab, owo$createSelectAction(owoGroup, tabIdx));\n                    if (owoGroup.isTabSelected(tabIdx)) tabButton.isSelected = true;\n\n                    this.owoButtons.add(tabButton);\n                    this.addRenderableWidget(tabButton);\n                }\n            }\n\n            final var buttonStackHeight = owoGroup.getButtonStackHeight();\n            tabRootY = this.topPos - 13 * (buttonStackHeight - 4);\n\n            var buttons = owoGroup.getButtons();\n            for (int i = 0; i < buttons.size(); i++) {\n                var buttonDefinition = buttons.get(i);\n\n                int xOffset = this.leftPos + 198 + (i / buttonStackHeight) * 26;\n                int yOffset = tabRootY + 10 + (i % buttonStackHeight) * 30;\n\n                var tabButton = new ItemGroupButtonWidget(xOffset, yOffset, 0, buttonDefinition, __ -> buttonDefinition.action().run());\n\n                this.owoButtons.add(tabButton);\n                this.addRenderableWidget(tabButton);\n            }\n        }\n    }\n\n    @Inject(at = @At(\"TAIL\"), method = \"render\")\n    private void render(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) {\n        boolean anyButtonHovered = false;\n\n        for (var button : this.owoButtons) {\n            if (button.trulyHovered()) {\n                context.setComponentTooltipForNextFrame(\n                    this.font,\n                    button.isTab() && ((OwoItemGroup) selectedTab).canSelectMultipleTabs()\n                        ? List.of(button.getMessage(), Component.translatable(\"text.owo.itemGroup.select_hint\"))\n                        : List.of(button.getMessage()),\n                    mouseX,\n                    mouseY,\n                    null\n                );\n                anyButtonHovered = true;\n            }\n        }\n\n        this.cursorAdapter.applyStyle(anyButtonHovered ? CursorStyle.HAND : CursorStyle.NONE);\n    }\n\n    @Inject(method = \"removed\", at = @At(\"HEAD\"))\n    private void disposeCursorAdapter(CallbackInfo ci) {\n        this.cursorAdapter.dispose();\n    }\n\n    @Override\n    public int owo$getRootX() {\n        return this.leftPos;\n    }\n\n    @Override\n    public int owo$getRootY() {\n        return this.topPos;\n    }\n\n    @Unique\n    private Consumer<ItemGroupButtonWidget> owo$createSelectAction(OwoItemGroup group, int tabIdx) {\n        return button -> {\n            var context = new CreativeModeTab.ItemDisplayParameters(this.enabledFeatures, this.hasPermissions(this.menu.player()), this.menu.player().level().registryAccess());\n\n            // cringe\n            var shift = InputConstants.isKeyDown(Minecraft.getInstance().getWindow(), GLFW.GLFW_KEY_LEFT_SHIFT)\n                || InputConstants.isKeyDown(Minecraft.getInstance().getWindow(), GLFW.GLFW_KEY_RIGHT_SHIFT);\n\n            if (shift) {\n                group.toggleTab(tabIdx, context);\n            } else {\n                group.selectSingleTab(tabIdx, context);\n            }\n\n            this.rebuildWidgets();\n            button.isSelected = true;\n        };\n    }\n\n    public CreativeModeInventoryScreenMixin(CreativeModeInventoryScreen.ItemPickerMenu screenHandler, Inventory playerInventory, Component text) {\n        super(screenHandler, playerInventory, text);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/CreativeModeTabAccessor.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.ItemStack;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.Set;\n\n@Mixin(CreativeModeTab.class)\npublic interface CreativeModeTabAccessor {\n\n    @Accessor(\"displayItemsGenerator\")\n    CreativeModeTab.DisplayItemsGenerator owo$getDisplayItemsGenerator();\n\n    @Mutable\n    @Accessor(\"displayItemsGenerator\")\n    void owo$setDisplayItemsGenerator(CreativeModeTab.DisplayItemsGenerator collector);\n\n    @Accessor(\"displayItemsSearchTab\")\n    void owo$setDisplayItemsSearchTab(Set<ItemStack> searchTabStacks);\n\n    @Mutable\n    @Accessor(\"displayName\")\n    void owo$setDisplayName(Component displayName);\n\n    @Mutable\n    @Accessor(\"column\")\n    void owo$setColumn(int column);\n\n    @Mutable\n    @Accessor(\"row\")\n    void owo$setRow(CreativeModeTab.Row row);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/EffectsInInventoryMixin.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;\nimport net.minecraft.client.gui.screens.inventory.EffectsInInventory;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.ModifyVariable;\n\n@Mixin(EffectsInInventory.class)\npublic class EffectsInInventoryMixin {\n\n    @ModifyVariable(method = \"renderEffects\",\n        at = @At(\"HEAD\"),\n        ordinal = 0, argsOnly = true)\n    private int shiftStatusEffects(int x) {\n        if (!((Object) this instanceof CreativeModeInventoryScreen)) return x;\n        if (!(CreativeModeInventoryScreenAccessor.owo$getSelectedTab() instanceof OwoItemGroup group)) return x;\n        if (group.getButtons().isEmpty()) return x;\n\n        return x + 28;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/ItemMixin.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.util.pond.OwoItemExtensions;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport org.jetbrains.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.function.BiConsumer;\n\n@Mixin(Item.class)\npublic class ItemMixin implements OwoItemExtensions {\n\n    @Nullable\n    protected CreativeModeTab owo$group = null;\n\n    @Unique\n    private int owo$tab = 0;\n\n    @Unique\n    private BiConsumer<Item, CreativeModeTab.Output> owo$stackGenerator;\n\n    @Unique\n    private boolean owo$trackUsageStat = false;\n\n    @Inject(method = \"<init>\", at = @At(\"TAIL\"))\n    private void grabTab(Item.Properties settings, CallbackInfo ci) {\n        this.owo$tab = settings.tab();\n        this.owo$stackGenerator = settings.stackGenerator();\n        this.owo$group = settings.group();\n        this.owo$trackUsageStat = settings.shouldTrackUsageStat();\n    }\n\n    @Override\n    public int owo$tab() {\n        return owo$tab;\n    }\n\n    @Override\n    public BiConsumer<Item, CreativeModeTab.Output> owo$stackGenerator() {\n        return this.owo$stackGenerator != null ? this.owo$stackGenerator : OwoItemGroup.DEFAULT_STACK_GENERATOR;\n    }\n\n    @Override\n    public void owo$setGroup(CreativeModeTab group) {\n        this.owo$group = group;\n    }\n\n    @Override\n    public @Nullable CreativeModeTab owo$group() {\n        return this.owo$group;\n    }\n\n    @Override\n    public boolean owo$shouldTrackUsageStat() {\n        return this.owo$trackUsageStat;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/ItemSettingsMixin.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport io.wispforest.owo.itemgroup.ItemGroupReference;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.itemgroup.OwoItemSettingsExtension;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport org.spongepowered.asm.mixin.Mixin;\n\nimport java.util.function.BiConsumer;\n\n@Mixin(Item.Properties.class)\npublic class ItemSettingsMixin implements OwoItemSettingsExtension {\n    private OwoItemGroup owo$group = null;\n    private int owo$tab = 0;\n    private BiConsumer<Item, CreativeModeTab.Output> owo$stackGenerator = null;\n    private boolean owo$trackUsageStat = false;\n\n    @Override\n    public Item.Properties group(ItemGroupReference ref) {\n        this.owo$group = ref.group();\n        this.owo$tab = ref.tab();\n\n        return (Item.Properties)(Object) this;\n    }\n\n    @Override\n    public Item.Properties group(OwoItemGroup group) {\n        this.owo$group = group;\n\n        return (Item.Properties)(Object) this;\n    }\n\n    @Override\n    public OwoItemGroup group() {\n        return owo$group;\n    }\n\n    @Override\n    public Item.Properties tab(int tab) {\n        this.owo$tab = tab;\n\n        return (Item.Properties)(Object) this;\n    }\n\n    @Override\n    public int tab() {\n        return owo$tab;\n    }\n\n    @Override\n    public Item.Properties stackGenerator(BiConsumer<Item, CreativeModeTab.Output> generator) {\n        this.owo$stackGenerator = generator;\n\n        return (Item.Properties)(Object) this;\n    }\n\n    @Override\n    public BiConsumer<Item, CreativeModeTab.Output> stackGenerator() {\n        return owo$stackGenerator;\n    }\n\n    @Override\n    public Item.Properties trackUsageStat() {\n        this.owo$trackUsageStat = true;\n\n        return (Item.Properties)(Object) this;\n    }\n\n    @Override\n    public boolean shouldTrackUsageStat() {\n        return owo$trackUsageStat;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/MinecraftMixin.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport io.wispforest.owo.Owo;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.Screen;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(Minecraft.class)\npublic abstract class MinecraftMixin {\n    @Shadow\n    protected abstract Thread getRunningThread();\n\n    @Inject(method = \"setScreen\", at = @At(value = \"HEAD\"))\n    private void preventOffThreadScreenSet(Screen screen, CallbackInfo ci) {\n        if (Thread.currentThread() != this.getRunningThread()) {\n            if (Owo.DEBUG) {\n                throw new IllegalStateException(\"Unable to invoke setScreen for '\" + screen.getClass().getName() + \"' as it was called not from the main thread! Please use `execute` on `Minecraft` instance.\");\n            } else {\n                Owo.LOGGER.error(\"Found setScreen for '{}' called off thread! Please use tell the developer to use `execute` on `Minecraft` instance to prevent issues.\", screen.getClass().getName());\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/itemgroup/MixinCreativeModeInventoryScreenMixin.java",
    "content": "package io.wispforest.owo.mixin.itemgroup;\n\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;\nimport net.fabricmc.fabric.api.client.itemgroup.v1.FabricCreativeInventoryScreen;\nimport net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;\nimport net.minecraft.world.item.CreativeModeTab;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@SuppressWarnings({\"MixinAnnotationTarget\", \"UnresolvedMixinReference\"})\n@Mixin(value = CreativeModeInventoryScreen.class, priority = 1100)\npublic abstract class MixinCreativeModeInventoryScreenMixin implements FabricCreativeInventoryScreen {\n\n    @Unique private static final Int2ObjectMap<CreativeModeTab> selectedTabForPage = new Int2ObjectOpenHashMap<>();\n\n    @Shadow\n    protected abstract void selectTab(CreativeModeTab group);\n\n    @Inject(method = \"selectTab\", at = @At(\"TAIL\"))\n    private void captureSetTab(CreativeModeTab group, CallbackInfo ci) {\n        selectedTabForPage.put(getCurrentPage(), group);\n    }\n\n    @Inject(method = \"updateSelection\", at = @At(\"HEAD\"), cancellable = true, remap = false)\n    private void yesThisMakesPerfectSenseAndIsVeryUsable(CallbackInfo ci) {\n        var selectedTab = selectedTabForPage.get(getCurrentPage());\n        if (selectedTab == null) return;\n        this.selectTab(selectedTab);\n        ci.cancel();\n    }\n\n    //---\n    // Code fixes some cases of an issue where current page value is somehow returned differently.\n    // Attempted to be resolve by using MinecraftMixin to prevent off thread screen set calls\n\n    /*\n    @Unique private static boolean calledFromInit = false;\n\n    @Shadow(remap = false) // FAPI\n    private static int currentPage;\n    @Shadow(remap = false) // FAPI\n    private void updateSelection() {}\n\n    @Inject(method = \"init\", at = @At(\"HEAD\"))\n    private void prepareTheFixForTheFix(CallbackInfo ci) {\n        calledFromInit = true;\n    }\n\n    @Inject(method = \"getCurrentPage\", at = @At(\"HEAD\"), cancellable = true)\n    private void iLoveFixingTheFix(CallbackInfoReturnable<Integer> cir) {\n        if (!calledFromInit) return;\n\n        cir.setReturnValue(currentPage);\n        calledFromInit = false;\n    }\n\n    @Inject(method = \"init\", at = @At(\"TAIL\"))\n    private void endTheFixForTheFix(CallbackInfo ci) {\n        this.updateSelection();\n        calledFromInit = false;\n    }\n    */\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/registry/MappedRegistryMixin.java",
    "content": "package io.wispforest.owo.mixin.registry;\n\nimport com.mojang.serialization.Lifecycle;\nimport io.wispforest.owo.util.OwoFreezer;\nimport io.wispforest.owo.util.pond.OwoSimpleRegistryExtensions;\nimport it.unimi.dsi.fastutil.objects.ObjectList;\nimport it.unimi.dsi.fastutil.objects.Reference2IntMap;\nimport net.fabricmc.fabric.api.event.registry.RegistryEntryAddedCallback;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.MappedRegistry;\nimport net.minecraft.core.RegistrationInfo;\nimport net.minecraft.core.WritableRegistry;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.resources.ResourceKey;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\n@Mixin(MappedRegistry.class)\npublic abstract class MappedRegistryMixin<T> implements WritableRegistry<T>, OwoSimpleRegistryExtensions<T> {\n\n    @Shadow private Map<T, Holder.Reference<T>> unregisteredIntrusiveHolders;\n    @Shadow @Final private Map<ResourceKey<T>, Holder.Reference<T>> byKey;\n    @Shadow @Final private Map<Identifier, Holder.Reference<T>> byLocation;\n    @Shadow @Final private Map<T, Holder.Reference<T>> byValue;\n    @Shadow @Final private ObjectList<Holder.Reference<T>> byId;\n    @Shadow @Final private Reference2IntMap<T> toId;\n    @Shadow @Final private Map<ResourceKey<T>, RegistrationInfo> registrationInfos;\n    @Shadow private Lifecycle registryLifecycle;\n\n    //--\n\n    /**\n     * Copy of the {@link MappedRegistry#register} function but uses {@link List#set} instead of {@link List#add} for {@link MappedRegistry#byId}\n     */\n    public Holder.Reference<T> owo$set(int id, ResourceKey<T> arg, T object, RegistrationInfo arg2) {\n        this.byValue.remove(object);\n\n        OwoFreezer.checkRegister(\"Registry Set Calls\"); //this.assertNotFrozen(arg);\n\n        Objects.requireNonNull(arg);\n        Objects.requireNonNull(object);\n\n        Holder.Reference<T> reference;\n\n        if (this.unregisteredIntrusiveHolders != null) {\n            reference = this.unregisteredIntrusiveHolders.remove(object);\n\n            if (reference == null) {\n                throw new AssertionError(\"Missing intrusive holder for \" + arg + \":\" + object);\n            }\n\n            ((ReferenceAccessor<T>) reference).owo$setRegistryKey(arg);\n        } else {\n            reference = this.byKey.computeIfAbsent(arg, k -> Holder.Reference.createStandAlone(this, k));\n            ((ReferenceAccessor<T>) reference).owo$setValue((T)object);\n        }\n\n        this.byKey.put(arg, reference);\n        this.byLocation.put(arg.identifier(), reference);\n        this.byValue.put(object, reference);\n        this.byId.set(id, reference);\n        this.toId.put(object, id);\n        this.registrationInfos.put(arg, arg2);\n        this.registryLifecycle = this.registryLifecycle.add(arg2.lifecycle());\n\n        // TODO: SHOULD WE BE REFIREING THE EVENT?\n        RegistryEntryAddedCallback.event(this).invoker().onEntryAdded(id, arg.identifier(), (T)object);\n\n        return reference;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/registry/ReferenceAccessor.java",
    "content": "package io.wispforest.owo.mixin.registry;\n\nimport net.minecraft.core.Holder;\nimport net.minecraft.resources.ResourceKey;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(Holder.Reference.class)\npublic interface ReferenceAccessor<T> {\n    @Invoker(\"bindKey\")\n    void owo$setRegistryKey(ResourceKey<T> registryKey);\n\n    @Invoker(\"bindValue\")\n    void owo$setValue(T value);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/CachedRegistryInfoGetterAccessor.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport net.minecraft.core.HolderLookup;\nimport net.minecraft.resources.RegistryOps;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(RegistryOps.HolderLookupAdapter.class)\npublic interface CachedRegistryInfoGetterAccessor {\n    @Accessor(\"lookupProvider\") HolderLookup.Provider owo$getRegistriesLookup();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/CompoundTagMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport io.wispforest.endec.SerializationAttributes;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.endec.util.MapCarrier;\nimport io.wispforest.owo.serialization.format.nbt.NbtDeserializer;\nimport io.wispforest.owo.serialization.format.nbt.NbtSerializer;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.Tag;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\n@Mixin(CompoundTag.class)\npublic abstract class CompoundTagMixin implements MapCarrier {\n\n    @Shadow\n    public abstract @Nullable Tag get(String key);\n    @Shadow\n    public abstract @Nullable Tag put(String key, Tag element);\n    @Shadow\n    public abstract @org.jspecify.annotations.Nullable Tag remove(String key);\n    @Shadow\n    public abstract boolean contains(String key);\n\n    @Override\n    public <T> T getWithErrors(SerializationContext ctx, @NotNull KeyedEndec<T> key) {\n        if (!this.has(key)) return key.defaultValue();\n        return key.endec().decodeFully(ctx.withAttributes(SerializationAttributes.HUMAN_READABLE), NbtDeserializer::of, this.get(key.key()));\n    }\n\n    @Override\n    public <T> void put(SerializationContext ctx, @NotNull KeyedEndec<T> key, @NotNull T value) {\n        this.put(key.key(), key.endec().encodeFully(ctx.withAttributes(SerializationAttributes.HUMAN_READABLE), NbtSerializer::of, value));\n    }\n\n    @Override\n    public <T> void delete(@NotNull KeyedEndec<T> key) {\n        this.remove(key.key());\n    }\n\n    @Override\n    public <T> boolean has(@NotNull KeyedEndec<T> key) {\n        return this.contains(key.key());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/DataComponentTypeBuilderMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport io.wispforest.owo.serialization.OwoDataComponentTypeBuilder;\nimport net.minecraft.core.component.DataComponentType;\nimport org.spongepowered.asm.mixin.Mixin;\n\n@Mixin(DataComponentType.Builder.class)\npublic abstract class DataComponentTypeBuilderMixin<T> implements OwoDataComponentTypeBuilder<T> {\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/DataResultMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.llamalad7.mixinextras.sugar.ref.LocalRef;\nimport com.mojang.logging.LogUtils;\nimport com.mojang.serialization.DataResult;\nimport com.mojang.serialization.Lifecycle;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.util.StackTraceSupplier;\nimport org.slf4j.Logger;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\nimport java.util.Optional;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n@Mixin(value = DataResult.class, remap = false)\npublic interface DataResultMixin {\n\n    @Inject(\n            method = {\n                \"error(Ljava/util/function/Supplier;)Lcom/mojang/serialization/DataResult;\",\n                \"error(Ljava/util/function/Supplier;Ljava/lang/Object;)Lcom/mojang/serialization/DataResult;\",\n                \"error(Ljava/util/function/Supplier;Lcom/mojang/serialization/Lifecycle;)Lcom/mojang/serialization/DataResult;\",\n                \"error(Ljava/util/function/Supplier;Ljava/lang/Object;Lcom/mojang/serialization/Lifecycle;)Lcom/mojang/serialization/DataResult;\"\n            },\n            at = @At(value = \"HEAD\"),\n            remap = false\n    )\n    private static <R> void wrapMessageWithStacktrace(CallbackInfoReturnable<Optional<DataResult.Error<R>>> cir, @Local(argsOnly = true) LocalRef<Supplier<String>> messageSupplier) {\n        if (!Owo.DEBUG) return;\n\n        var ogSupplier = messageSupplier.get();\n        var ogClass = ogSupplier.getClass();\n        if (ogSupplier instanceof StackTraceSupplier) return;\n\n        StackTraceSupplier stackTraceSupplier = null;\n\n        if (ogClass.isSynthetic()) {\n            try {\n                for (var field : ogClass.getDeclaredFields()) {\n                    if (!Throwable.class.isAssignableFrom(field.getType())) continue;\n\n                    field.setAccessible(true);\n                    if (field.get(ogSupplier) instanceof Throwable e) {\n                        stackTraceSupplier = StackTraceSupplier.of(e, ogSupplier);\n                    }\n                    break;\n                }\n            } catch (IllegalArgumentException | IllegalAccessException ignore) {}\n        }\n\n        if (stackTraceSupplier == null) stackTraceSupplier = StackTraceSupplier.of(ogSupplier.get());\n\n        messageSupplier.set(stackTraceSupplier);\n    }\n\n    @Mixin(value = DataResult.Error.class, remap = false)\n    abstract class DataResultErrorMixin<R> {\n\n        @Unique\n        private static final Logger LOGGER = LogUtils.getLogger();\n\n        @Shadow(remap = false)\n        public abstract Supplier<String> messageSupplier();\n\n        @Shadow @Final\n        private Supplier<String> messageSupplier;\n\n        @Inject(method = {\"getOrThrow\", \"getPartialOrThrow\"}, at = @At(value = \"HEAD\"), remap = false)\n        private <E extends Throwable> void addStackTraceToException(CallbackInfoReturnable<R> cir, @Local(argsOnly = true) LocalRef<Function<String, E>> exceptionSupplier) {\n            final var funcToWrap = exceptionSupplier.get();\n\n            exceptionSupplier.set(s -> {\n                var exception = funcToWrap.apply(s);\n                if (this.messageSupplier() instanceof StackTraceSupplier stackTraceSupplier) {\n                    exception.setStackTrace(stackTraceSupplier.getFullStackTrace());\n                }\n                return exception;\n            });\n        }\n\n        @WrapOperation(method ={\n            \"resultOrPartial(Ljava/util/function/Consumer;)Ljava/util/Optional;\",\n            \"promotePartial\"\n        }, at = @At(value = \"INVOKE\", target = \"Ljava/util/function/Consumer;accept(Ljava/lang/Object;)V\"))\n        private <T> void printStackTrace(Consumer<T> instance, T t, Operation<Void> original) {\n            original.call(instance, t);\n\n            if (Owo.DEBUG && this.messageSupplier instanceof StackTraceSupplier supplier) {\n                LOGGER.error(\"An error has occurred within DFU: \", supplier.throwable());\n            }\n        }\n\n        @WrapOperation(method = {\n            \"ap(Lcom/mojang/serialization/DataResult;)Lcom/mojang/serialization/DataResult$Error;\",\n            \"flatMap(Ljava/util/function/Function;)Lcom/mojang/serialization/DataResult$Error;\"\n        }, at = @At(value = \"NEW\", target = \"(Ljava/util/function/Supplier;Ljava/util/Optional;Lcom/mojang/serialization/Lifecycle;)Lcom/mojang/serialization/DataResult$Error;\", ordinal = 1))\n        private DataResult.Error preserveStackTrace1(Supplier<String> messageSupplier, Optional partialValue, Lifecycle lifecycle, Operation<DataResult.Error> original) {\n            if (this.messageSupplier instanceof StackTraceSupplier supplier) {\n                messageSupplier = StackTraceSupplier.of(supplier.throwable(), messageSupplier);\n            }\n\n            return original.call(messageSupplier, partialValue, lifecycle);\n        }\n\n        @WrapOperation(method = {\n            \"mapError(Ljava/util/function/UnaryOperator;)Lcom/mojang/serialization/DataResult$Error;\"\n        }, at = @At(value = \"NEW\", target = \"(Ljava/util/function/Supplier;Ljava/util/Optional;Lcom/mojang/serialization/Lifecycle;)Lcom/mojang/serialization/DataResult$Error;\"))\n        private DataResult.Error preserveStackTrace2(Supplier<String> messageSupplier, Optional partialValue, Lifecycle lifecycle, Operation<DataResult.Error> original) {\n            if (this.messageSupplier instanceof StackTraceSupplier supplier) {\n                messageSupplier = StackTraceSupplier.of(supplier.throwable(), messageSupplier);\n            }\n\n            return original.call(messageSupplier, partialValue, lifecycle);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/DelegatingOpsAccessor.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport com.mojang.serialization.DynamicOps;\nimport net.minecraft.resources.DelegatingOps;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(DelegatingOps.class)\npublic interface DelegatingOpsAccessor<T> {\n    @Accessor(\"delegate\")\n    DynamicOps<T> owo$delegate();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/FriendlyByteBufMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.format.bytebuf.ByteBufDeserializer;\nimport io.wispforest.endec.format.bytebuf.ByteBufSerializer;\nimport io.wispforest.endec.util.EndecBuffer;\nimport net.minecraft.network.FriendlyByteBuf;\nimport org.spongepowered.asm.mixin.Mixin;\n\n@SuppressWarnings({\"DataFlowIssue\"})\n@Mixin(FriendlyByteBuf.class)\npublic abstract class FriendlyByteBufMixin implements EndecBuffer {\n    @Override\n    public <T> void write(SerializationContext ctx, Endec<T> endec, T value) {\n        endec.encodeFully(ctx, () -> ByteBufSerializer.of((FriendlyByteBuf) (Object) this), value);\n    }\n\n    @Override\n    public <T> T read(SerializationContext ctx, Endec<T> endec) {\n        return endec.decodeFully(ctx, ByteBufDeserializer::of, (FriendlyByteBuf) (Object) this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/RegistryOpsAccessor.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport net.minecraft.resources.RegistryOps;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(RegistryOps.class)\npublic interface RegistryOpsAccessor {\n    @Accessor(\"lookupProvider\")\n    RegistryOps.RegistryInfoLookup owo$infoGetter();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/TagValueInputMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.endec.util.MapCarrierDecodable;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.KeyedEndecDecodeError;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.world.level.storage.TagValueInput;\nimport net.minecraft.world.level.storage.ValueInputContextHelper;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\n@Mixin(TagValueInput.class)\npublic abstract class TagValueInputMixin implements MapCarrierDecodable {\n    @Shadow\n    @Final\n    private CompoundTag input;\n\n    @Shadow\n    @Final\n    private ProblemReporter problemReporter;\n\n    @Shadow\n    @Final\n    private ValueInputContextHelper context;\n\n    // TODO: Maybe pass in the ErrorReporter for use within Endecs?\n    @Override\n    public <T> T getWithErrors(SerializationContext ctx, @NotNull KeyedEndec<T> key) {\n        ctx = CodecUtils.createContext(this.context.ops(), ctx);\n\n        return this.input.getWithErrors(ctx, key);\n    }\n\n    @Override\n    public <T> T get(SerializationContext ctx, @NotNull KeyedEndec<T> key) {\n        try {\n            return this.getWithErrors(ctx, key);\n        } catch (Exception e) {\n            this.problemReporter.report(new KeyedEndecDecodeError(key, this.input.get(key.key()), e));\n\n            return key.defaultValue();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/TagValueOutputMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport com.mojang.serialization.DynamicOps;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.endec.util.MapCarrierEncodable;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.KeyedEndecEncodeError;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.Tag;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.world.level.storage.TagValueOutput;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\n@Mixin(TagValueOutput.class)\npublic abstract class TagValueOutputMixin implements MapCarrierEncodable {\n    @Shadow\n    @Final\n    private CompoundTag output;\n\n    @Shadow\n    @Final\n    private ProblemReporter problemReporter;\n\n    @Shadow\n    @Final\n    private DynamicOps<Tag> ops;\n\n    @Override\n    public <T> void put(SerializationContext ctx, @NotNull KeyedEndec<T> key, @NotNull T value) {\n        ctx = CodecUtils.createContext(this.ops, ctx);\n\n        try {\n            this.output.put(ctx, key, value);\n        } catch (Exception e) {\n            problemReporter.report(new KeyedEndecEncodeError(key, value, e, false));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/ValueInputMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport com.mojang.serialization.Codec;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.endec.util.MapCarrierDecodable;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport net.fabricmc.fabric.api.serialization.v1.view.FabricReadView;\nimport net.minecraft.world.level.storage.ValueInput;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\nimport java.util.Optional;\n\n@Mixin(ValueInput.class)\npublic interface ValueInputMixin extends MapCarrierDecodable, FabricReadView {\n    @Shadow\n    <T> Optional<T> read(String key, Codec<T> codec);\n\n    @Override\n    default <T> T getWithErrors(SerializationContext ctx, @NotNull KeyedEndec<T> key) {\n        return this.read(key.key(), CodecUtils.toCodec(key.endec(), ctx))\n            .orElseGet(key::defaultValue);\n    }\n\n    @Override\n    default <T> boolean has(@NotNull KeyedEndec<T> key) {\n        return this.contains(key.key());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/serialization/ValueOutputMixin.java",
    "content": "package io.wispforest.owo.mixin.serialization;\n\nimport com.mojang.serialization.Codec;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.endec.util.MapCarrierEncodable;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport net.minecraft.world.level.storage.ValueOutput;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\n@Mixin(ValueOutput.class)\npublic interface ValueOutputMixin extends MapCarrierEncodable {\n\n    @Shadow <T> void store(String key, Codec<T> codec, T value);\n\n    @Shadow void discard(String key);\n\n    @Override\n    default <T> void put(SerializationContext ctx, @NotNull KeyedEndec<T> key, @NotNull T value) {\n        this.store(key.key(), CodecUtils.toCodec(key.endec(), ctx), value);\n    }\n\n    @Override\n    default <T> void delete(@NotNull KeyedEndec<T> key) {\n        this.discard(key.key());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/shader/GlProgramAccessor.java",
    "content": "package io.wispforest.owo.mixin.shader;\n\nimport com.mojang.blaze3d.opengl.GlProgram;\nimport com.mojang.blaze3d.opengl.Uniform;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\nimport java.util.Map;\n\n@Mixin(GlProgram.class)\npublic interface GlProgramAccessor {\n\n    @Accessor(\"uniformsByName\")\n    Map<String, Uniform> owo$getUniformsByName();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/text/ClientLanguageMixin.java",
    "content": "package io.wispforest.owo.mixin.text;\n\nimport com.google.common.collect.ImmutableMap;\nimport com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport io.wispforest.owo.text.LanguageAccess;\nimport io.wispforest.owo.text.TextLanguage;\nimport io.wispforest.owo.util.KawaiiUtil;\nimport net.minecraft.client.resources.language.ClientLanguage;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport org.spongepowered.asm.mixin.*;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\n@Debug(export = true)\n@Mixin(ClientLanguage.class)\npublic class ClientLanguageMixin implements TextLanguage {\n\n    @Mutable\n    @Shadow\n    @Final\n    private Map<String, String> storage;\n\n    private final Map<String, Component> owo$textMap = new HashMap<>();\n\n    @Inject(method = \"<init>\", at = @At(\"TAIL\"))\n    private void kawaii(Map<String, String> translations, boolean rightToLeft, CallbackInfo ci) {\n        if (!Objects.equals(System.getProperty(\"owo.uwu\"), \"yes please\")) return;\n\n        var builder = ImmutableMap.<String, String>builder();\n        translations.forEach((s, s2) -> builder.put(s, KawaiiUtil.uwuify(s2)));\n        this.storage = builder.build();\n    }\n\n    @WrapMethod(method = \"loadFrom\")\n    private static ClientLanguage setupAndSetText(ResourceManager resourceManager, List<String> list, boolean bl, Operation<ClientLanguage> original) {\n        var buildingMap = new HashMap<String, Component>();\n        LanguageAccess.textConsumer.set(buildingMap::put);\n        var lang = original.call(resourceManager, list, bl);\n        LanguageAccess.textConsumer.remove();\n        var map = ((ClientLanguageMixin) (Object) lang).owo$textMap;\n        map.clear();\n        map.putAll(buildingMap);\n        return lang;\n    }\n\n    @Inject(method = \"has\", at = @At(\"HEAD\"), cancellable = true)\n    private void hasTranslation(String key, CallbackInfoReturnable<Boolean> cir) {\n        if (this.owo$textMap.containsKey(key)) cir.setReturnValue(true);\n    }\n\n    @Inject(method = \"getOrDefault\", at = @At(\"HEAD\"), cancellable = true)\n    private void get(String key, String fallback, CallbackInfoReturnable<String> cir) {\n        if (this.owo$textMap.containsKey(key)) cir.setReturnValue(this.owo$textMap.get(key).getString());\n    }\n\n    @Override\n    public Component getText(String key) {\n        return this.owo$textMap.get(key);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/text/ComponentSerializationMixin.java",
    "content": "package io.wispforest.owo.mixin.text;\n\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.MapCodec;\nimport io.wispforest.owo.text.CustomTextRegistry;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.ComponentContents;\nimport net.minecraft.network.chat.ComponentSerialization;\nimport net.minecraft.util.ExtraCodecs;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(ComponentSerialization.class)\npublic abstract class ComponentSerializationMixin {\n\n    @Inject(method = \"createCodec\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/network/chat/ComponentSerialization;bootstrap(Lnet/minecraft/util/ExtraCodecs$LateBoundIdMapper;)V\", shift = At.Shift.AFTER))\n    private static void injectOwoCodecs(Codec<Component> selfCodec, CallbackInfoReturnable<Codec<Component>> cir, @Local ExtraCodecs.LateBoundIdMapper<String, MapCodec<? extends ComponentContents>> mapper) {\n        CustomTextRegistry.inject(mapper);\n    }\n\n}\n\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/text/LanguageMixin.java",
    "content": "package io.wispforest.owo.mixin.text;\n\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParseException;\nimport com.google.gson.JsonPrimitive;\nimport com.llamalad7.mixinextras.injector.v2.WrapWithCondition;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.llamalad7.mixinextras.sugar.Share;\nimport com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;\nimport com.mojang.serialization.JsonOps;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.text.CursedTranslatableContents;\nimport io.wispforest.owo.text.LanguageAccess;\nimport io.wispforest.owo.text.NestedLangHandler;\nimport io.wispforest.owo.util.DataExtensionUtil;\nimport net.minecraft.locale.Language;\nimport net.minecraft.network.chat.ComponentSerialization;\nimport net.minecraft.network.chat.MutableComponent;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\n\nimport java.io.InputStream;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.BiConsumer;\n\n@Mixin(Language.class)\npublic class LanguageMixin {\n\n    @Unique private static final String RICH_TRANSLATIONS_ENABLER = \"rich_translations\";\n    @Unique private static final String NESTED_LANG_ENABLER = \"nested_lang\";\n\n    @WrapOperation(\n        method = \"loadFromJson(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V\",\n        at = @At(value = \"INVOKE\", target = \"Lcom/google/gson/JsonObject;entrySet()Ljava/util/Set;\")\n    )\n    private static Set<Map.Entry<String, JsonElement>> deNestNestedKeys(\n        JsonObject instance,\n        Operation<Set<Map.Entry<String, JsonElement>>> original,\n        @Share(RICH_TRANSLATIONS_ENABLER) LocalBooleanRef richTranslationsEnabled,\n        @Share(NESTED_LANG_ENABLER) LocalBooleanRef nestedLangEnabled,\n        @Local(argsOnly = true) InputStream stream\n    ) {\n        var enabledByDefault = featureEnabled(instance, \"extended_lang\", false) || stream instanceof DataExtensionUtil.CoercedByteArrayInputStream;\n        richTranslationsEnabled.set(featureEnabled(instance, RICH_TRANSLATIONS_ENABLER, enabledByDefault));\n        nestedLangEnabled.set(featureEnabled(instance, NESTED_LANG_ENABLER, enabledByDefault));\n        return nestedLangEnabled.get() ? NestedLangHandler.deNest(original.call(instance)) : original.call(instance);\n    }\n\n    @Unique\n    private static boolean featureEnabled(JsonObject instance, String feature, boolean defaultValue) {\n        feature = Owo.id(feature).toString();\n        var value = instance.get(feature);\n        if (!(value instanceof JsonPrimitive primitive)) return defaultValue;\n        instance.remove(feature);\n        if (primitive.isNumber()) return primitive.getAsNumber().doubleValue() != 0;\n        return primitive.getAsBoolean();\n    }\n\n    @Unique private static final String SKIP_NEXT = \"skipNextKey\";\n\n    @WrapOperation(\n        method = \"loadFromJson(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V\", at = @At(\n        value = \"INVOKE\",\n        target = \"Lnet/minecraft/util/GsonHelper;convertToString(Lcom/google/gson/JsonElement;Ljava/lang/String;)Ljava/lang/String;\"\n    )\n    )\n    private static String handleRichTranslationsAndErrors(\n        JsonElement element,\n        String name,\n        Operation<String> original,\n        @Share(RICH_TRANSLATIONS_ENABLER) LocalBooleanRef richTranslationsEnabled,\n        @Share(NESTED_LANG_ENABLER) LocalBooleanRef nestedLangEnabled,\n        @Share(SKIP_NEXT) LocalBooleanRef skipNext\n    ) {\n        skipNext.set(false);\n        var rich = richTranslationsEnabled.get();\n        if (rich || nestedLangEnabled.get()) {\n            try {\n                var consumer = LanguageAccess.textConsumer.get();\n                if (rich && !element.isJsonPrimitive() && consumer != LanguageAccess.EMPTY_CONSUMER) {\n                    skipNext.set(true);\n\n                    MutableComponent text = (MutableComponent) ComponentSerialization.CODEC\n                        .parse(JsonOps.INSTANCE, element)\n                        .getOrThrow(JsonParseException::new);\n                    consumer.accept(name, CursedTranslatableContents.unpackArgs(text));\n\n                    return \"\";\n                } else if (element.isJsonPrimitive()) {\n                    return original.call(element, name);\n                } else {\n                    skipNext.set(true);\n                    return \"\";\n                }\n            } catch (Exception e) {\n                skipNext.set(true);\n                Owo.LOGGER.error(\n                    \"Preventing language loading from failing due to invalid key \\\"{}\\\"\\n{}\",\n                    name,\n                    e.getMessage()\n                );\n                return \"\";\n            }\n        }\n        return original.call(element, name);\n    }\n\n    @WrapWithCondition(\n        method = \"loadFromJson(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V\",\n        at = @At(value = \"INVOKE\", target = \"Ljava/util/function/BiConsumer;accept(Ljava/lang/Object;Ljava/lang/Object;)V\"))\n    private static boolean doSkip(\n        BiConsumer<Object, Object> biConsumer,\n        Object t, Object u,\n        @Share(SKIP_NEXT) LocalBooleanRef skipNext\n    ) {\n        return !skipNext.get();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/text/TranslatableContentsAccessor.java",
    "content": "package io.wispforest.owo.mixin.text;\n\nimport net.minecraft.network.chat.FormattedText;\nimport net.minecraft.network.chat.contents.TranslatableContents;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\nimport java.util.function.Consumer;\n\n@Mixin(TranslatableContents.class)\npublic interface TranslatableContentsAccessor {\n    @Invoker(\"decomposeTemplate\")\n    void owo$decomposeTemplate(String translation, Consumer<FormattedText> partsConsumer);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/text/TranslatableContentsMixin.java",
    "content": "package io.wispforest.owo.mixin.text;\n\nimport com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.text.CursedTranslatableContents;\nimport io.wispforest.owo.text.InsertingTextContent;\nimport io.wispforest.owo.text.TextLanguage;\nimport io.wispforest.owo.text.TranslationContext;\nimport net.minecraft.locale.Language;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.FormattedText;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.chat.contents.TranslatableContents;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.ModifyVariable;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\n@Mixin(TranslatableContents.class)\npublic class TranslatableContentsMixin {\n    @Shadow private List<FormattedText> decomposedParts;\n\n    @Shadow\n    @Final\n    private String key;\n\n    @Inject(method = {\"visit(Lnet/minecraft/network/chat/FormattedText$ContentConsumer;)Ljava/util/Optional;\", \"visit(Lnet/minecraft/network/chat/FormattedText$StyledContentConsumer;Lnet/minecraft/network/chat/Style;)Ljava/util/Optional;\"}, at = @At(value = \"INVOKE\", target = \"Ljava/util/List;iterator()Ljava/util/Iterator;\"), cancellable = true)\n    private <T> void enter(CallbackInfoReturnable<Optional<T>> cir) {\n        if (!TranslationContext.pushContent((TranslatableContents) (Object) this)) {\n            Owo.LOGGER.warn(\"Detected translation reference cycle, replacing with empty\");\n            cir.setReturnValue(Optional.empty());\n        }\n    }\n\n    @Inject(method = {\"visit(Lnet/minecraft/network/chat/FormattedText$ContentConsumer;)Ljava/util/Optional;\", \"visit(Lnet/minecraft/network/chat/FormattedText$StyledContentConsumer;Lnet/minecraft/network/chat/Style;)Ljava/util/Optional;\"}, at = @At(value = \"RETURN\"))\n    private <T> void exit(CallbackInfoReturnable<Optional<T>> cir) {\n        TranslationContext.popContent();\n    }\n\n    @Inject(method = \"decompose\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/locale/Language;getOrDefault(Ljava/lang/String;)Ljava/lang/String;\"), cancellable = true)\n    private void pullTranslationText(CallbackInfo ci) {\n        Language lang = Language.getInstance();\n        if (lang instanceof TextLanguage) {\n            Component text = ((TextLanguage) lang).getText(key);\n\n            if (text != null) {\n                decomposedParts = new ArrayList<>();\n                decomposedParts.add(text);\n                ci.cancel();\n            }\n        }\n    }\n\n    @ModifyVariable(\n        method = \"decomposeTemplate\",\n        at = @At(\n            value = \"STORE\",\n            ordinal = 0\n        )\n    )\n    private int restoreCorrectArgIndex(int value) {\n        return ((Object)this) instanceof CursedTranslatableContents ? CursedTranslatableContents.argIndex : value;\n    }\n\n    @Inject(\n        method = \"decomposeTemplate\",\n        at = @At(\n            value = \"INVOKE\",\n            target = \"Lnet/minecraft/network/chat/contents/TranslatableContents;getArgument(I)Lnet/minecraft/network/chat/FormattedText;\"\n        )\n    )\n    private void keepCorrectArgIndex(CallbackInfo ci, @Local(ordinal = 0) int argIndex) {\n        if (((Object)this) instanceof CursedTranslatableContents) CursedTranslatableContents.argIndex = argIndex;\n    }\n\n    @WrapMethod(method = \"getArgument\")\n    private FormattedText unpackArgs(int index, Operation<FormattedText> original) {\n        return ((Object)this) instanceof CursedTranslatableContents ? MutableComponent.create(new InsertingTextContent(index)) : original.call(index);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/text/stapi/SystemDelegatedLanguageFixin.java",
    "content": "package io.wispforest.owo.mixin.text.stapi;\n\nimport io.wispforest.owo.text.TextLanguage;\nimport net.minecraft.locale.Language;\nimport net.minecraft.network.chat.Component;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Pseudo;\nimport org.spongepowered.asm.mixin.Shadow;\nimport xyz.nucleoid.server.translations.api.language.ServerLanguage;\nimport xyz.nucleoid.server.translations.impl.language.SystemDelegatedLanguage;\n\n@Pseudo\n@Mixin(SystemDelegatedLanguage.class)\npublic abstract class SystemDelegatedLanguageFixin implements TextLanguage {\n    @Final\n    @Shadow private Language vanilla;\n\n    @Shadow\n    protected abstract ServerLanguage getSystemLanguage();\n\n    @Override\n    public Component getText(String key) {\n        if (!(vanilla instanceof TextLanguage lang) || this.getSystemLanguage().serverTranslations().contains(key)) {\n            return null;\n        }\n\n        return lang.getText(key);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/tweaks/EditBoxMixin.java",
    "content": "package io.wispforest.owo.mixin.tweaks;\n\nimport io.wispforest.owo.Owo;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.network.chat.Component;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(EditBox.class)\npublic abstract class EditBoxMixin extends AbstractWidget {\n\n    @Shadow\n    private String value;\n\n    public EditBoxMixin(int x, int y, int width, int height, Component message) {\n        super(x, y, width, height, message);\n    }\n\n    @Inject(method = \"getWordPosition(IIZ)I\", at = @At(\"HEAD\"), cancellable = true)\n    private void iProvideUsefulSeparators(int wordOffset, int cursorPosition, boolean skipOverSpaces, CallbackInfoReturnable<Integer> cir) {\n        if (!Owo.DEBUG) return;\n\n        int wordsToSkip = Math.abs(wordOffset);\n        boolean forward = wordOffset > 0;\n\n        for (int i = 0; i < wordsToSkip; i++) {\n            if (forward) {\n                cursorPosition++;\n                while (cursorPosition < this.value.length() && owo$isWordChar(this.value.charAt(cursorPosition))) cursorPosition++;\n            } else if (cursorPosition > 0) {\n                cursorPosition--;\n                while (cursorPosition > 0 && owo$isWordChar(this.value.charAt(cursorPosition - 1))) cursorPosition--;\n            }\n        }\n\n        cir.setReturnValue(cursorPosition);\n    }\n\n    @Unique\n    private boolean owo$isWordChar(char charAt) {\n        return charAt == '_' || Character.isAlphabetic(charAt) || Character.isDigit(charAt);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/tweaks/EulaMixin.java",
    "content": "package io.wispforest.owo.mixin.tweaks;\n\nimport net.minecraft.server.Eula;\nimport org.slf4j.Logger;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Properties;\nimport java.util.Scanner;\n\n@Mixin(Eula.class)\npublic abstract class EulaMixin {\n\n    @Shadow\n    @Final\n    private static Logger LOGGER;\n\n    @Shadow\n    @Final\n    private Path file;\n\n    @Shadow public abstract boolean hasAgreedToEULA();\n\n    @Inject(method = \"readFile\", at = @At(value = \"TAIL\"), cancellable = true)\n    private void overrideEulaAgreement(CallbackInfoReturnable<Boolean> cir) {\n        if (this.hasAgreedToEULA()) return;\n\n        var scanner = new Scanner(System.in);\n        LOGGER.info(\"By answering 'true' to this prompt you are indicating your agreement to Minecraft's EULA (https://account.mojang.com/documents/minecraft_eula)\\nEULA:\");\n\n        var input = scanner.next();\n        if (!input.equalsIgnoreCase(\"true\")) return;\n\n        try (var inStream = Files.newInputStream(this.file); var outStream = Files.newOutputStream(this.file)) {\n            var properties = new Properties();\n            properties.load(inStream);\n            properties.setProperty(\"eula\", \"true\");\n            properties.store(outStream, \"By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\");\n        } catch (IOException e) {\n            LOGGER.info(\"Could not accept eula\", e);\n        }\n\n        LOGGER.info(\"EULA accepted\");\n        cir.setReturnValue(true);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/tweaks/LevelSettingsMixin.java",
    "content": "package io.wispforest.owo.mixin.tweaks;\n\nimport io.wispforest.owo.Owo;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.world.Difficulty;\nimport net.minecraft.world.level.GameType;\nimport net.minecraft.world.level.LevelSettings;\nimport net.minecraft.world.level.WorldDataConfiguration;\nimport net.minecraft.world.level.gamerules.GameRules;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(LevelSettings.class)\npublic class LevelSettingsMixin {\n\n    @Shadow\n    @Final\n    private GameRules gameRules;\n\n    @Inject(method = \"<init>\", at = @At(\"TAIL\"))\n    private void simulationIsForNerds(String name, GameType gameMode, boolean hardcore, Difficulty difficulty, boolean allowCommands, GameRules gameRules, WorldDataConfiguration dataConfiguration, CallbackInfo ci) {\n        if (!(Owo.DEBUG && FabricLoader.getInstance().isDevelopmentEnvironment())) return;\n\n        this.gameRules.set(GameRules.ADVANCE_TIME, false, null);\n        this.gameRules.set(GameRules.ADVANCE_WEATHER, false, null);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/tweaks/OperatingSystemMixin.java",
    "content": "package io.wispforest.owo.mixin.tweaks;\n\nimport com.mojang.logging.LogUtils;\nimport net.minecraft.util.Util;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Overwrite;\nimport org.spongepowered.asm.mixin.Shadow;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.util.concurrent.CompletableFuture;\n\n@Mixin(value = Util.OS.class)\npublic abstract class OperatingSystemMixin {\n\n    @Shadow protected abstract String[] getOpenUriArguments(URI uri);\n\n    /**\n     * @author glisco\n     * @reason By not properly consuming the stdout stream of the started process,\n     * Minecraft's implementation of this method causes xdg-open on linux to fail at actually\n     * opening the target program about 80% of the time. This overwrite uses a more modern approach\n     * to starting processes and properly voids both stdout and stderr, making xdg-open succeed\n     * at opening the user's desired application 100% of the time\n     */\n    @Overwrite()\n    public void openUri(URI uri) {\n        CompletableFuture.runAsync(() -> {\n            try {\n                final var command = getOpenUriArguments(uri);\n                new ProcessBuilder(command)\n                        .redirectError(ProcessBuilder.Redirect.DISCARD)\n                        .redirectOutput(ProcessBuilder.Redirect.DISCARD)\n                        .start();\n            } catch (IOException e) {\n                LogUtils.getLogger().error(\"Couldn't open uri '{}'\", uri, e);\n            }\n        }, Util.backgroundExecutor());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/AbstractContainerScreenMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.mojang.blaze3d.opengl.GlStateManager;\nimport io.wispforest.owo.ui.base.BaseOwoContainerScreen;\nimport io.wispforest.owo.util.pond.OwoSlotExtension;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.inventory.Slot;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.ModifyVariable;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(AbstractContainerScreen.class)\npublic abstract class AbstractContainerScreenMixin extends Screen {\n\n    @Unique\n    private static boolean inOwoScreen = false;\n\n    protected AbstractContainerScreenMixin(Component title) {\n        super(title);\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    @Inject(method = \"render\", at = @At(\"HEAD\"))\n    private void captureOwoState(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) {\n        inOwoScreen = (Object) this instanceof BaseOwoContainerScreen<?, ?>;\n    }\n\n    @Inject(method = \"render\", at = @At(\"TAIL\"))\n    private void resetOwoState(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) {\n        inOwoScreen = false;\n    }\n\n    @Inject(method = \"renderSlot\", at = @At(\"HEAD\"))\n    private void injectSlotScissors(GuiGraphics context, Slot slot, int mouseX, int mouseY, CallbackInfo ci) {\n        if (!inOwoScreen) return;\n\n        var scissorArea = ((OwoSlotExtension) slot).owo$getScissorArea();\n        if (scissorArea == null) return;\n\n        GlStateManager._enableScissorTest();\n        GlStateManager._scissorBox(scissorArea.x(), scissorArea.y(), scissorArea.width(), scissorArea.height());\n    }\n\n    @Inject(method = \"renderSlot\", at = @At(\"RETURN\"))\n    private void clearSlotScissors(GuiGraphics context, Slot slot, int mouseX, int mouseY, CallbackInfo ci) {\n        if (!inOwoScreen) return;\n\n        var scissorArea = ((OwoSlotExtension) slot).owo$getScissorArea();\n        if (scissorArea == null) return;\n\n        GlStateManager._disableScissorTest();\n    }\n\n    @ModifyVariable(method = \"mouseClicked\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/OptionInstance;get()Ljava/lang/Object;\", ordinal = 0), ordinal = 2)\n    private int doNoThrow(int slotId, @Local() Slot slot) {\n        return (((Object) this instanceof BaseOwoContainerScreen<?, ?>) && slot != null) ? slot.index : slotId;\n    }\n\n    @Inject(method = \"keyPressed\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;checkHotbarKeyPressed(Lnet/minecraft/client/input/KeyEvent;)Z\"), cancellable = true)\n    private void closeIt(KeyEvent input, CallbackInfoReturnable<Boolean> cir) {\n        if (!((Object) this instanceof BaseOwoContainerScreen<?, ?>)) return;\n\n        if (input.isEscape() && this.shouldCloseOnEsc()) {\n            this.onClose();\n            cir.setReturnValue(true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/AbstractSliderButtonMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport io.wispforest.owo.ui.component.DiscreteSliderComponent;\nimport io.wispforest.owo.ui.component.SliderComponent;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport net.minecraft.client.gui.components.AbstractSliderButton;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.ModifyArg;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\n\n@SuppressWarnings(\"ConstantConditions\")\n@Mixin(AbstractSliderButton.class)\npublic abstract class AbstractSliderButtonMixin extends AbstractWidget {\n    @Shadow\n    protected abstract void setValue(double value);\n\n    @Shadow protected double value;\n\n    public AbstractSliderButtonMixin(int x, int y, int width, int height, Component message) {\n        super(x, y, width, height, message);\n    }\n\n    @ModifyArg(method = \"keyPressed\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/components/AbstractSliderButton;setValue(D)V\"))\n    private double injectCustomStep(double value) {\n        if (!((Object) this instanceof SliderComponent slider)) return value;\n        return this.value + Math.signum(value - this.value) * slider.scrollStep();\n    }\n\n    @Inject(method = \"setValueFromMouse\", at = @At(\"HEAD\"), cancellable = true)\n    private void makeItSnappyTeam(MouseButtonEvent click, CallbackInfo ci) {\n        if (!((Object) this instanceof DiscreteSliderComponent discrete)) return;\n        if (!discrete.snap()) return;\n\n        ci.cancel();\n\n        double value = (click.x() - (this.getX() + 4d)) / (this.width - 8d);\n        double min = discrete.min(), max = discrete.max();\n        int decimalPlaces = discrete.decimalPlaces();\n\n        this.setValue(\n                (new BigDecimal(min + value * (max - min)).setScale(decimalPlaces, RoundingMode.HALF_UP).doubleValue() - min) / (max - min)\n        );\n    }\n\n    protected CursorStyle owo$preferredCursorStyle() {\n        return CursorStyle.MOVE;\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/AbstractWidgetMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.component.VanillaWidgetComponent;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.event.*;\nimport io.wispforest.owo.ui.inject.UIComponentStub;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.FocusHandler;\nimport io.wispforest.owo.util.EventSource;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.jetbrains.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.w3c.dom.Element;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n@SuppressWarnings(\"ConstantConditions\")\n@Mixin(AbstractWidget.class)\npublic abstract class AbstractWidgetMixin implements UIComponentStub, net.minecraft.client.gui.components.events.GuiEventListener {\n\n    @Shadow public boolean active;\n\n    @Shadow protected boolean isHovered;\n\n    @Unique\n    protected VanillaWidgetComponent wrapper = null;\n\n    @Override\n    public void inflate(Size space) {\n        this.owo$getWrapper().inflate(space);\n    }\n\n    @Override\n    public void mount(ParentUIComponent parent, int x, int y) {\n        this.owo$getWrapper().mount(parent, x, y);\n    }\n\n    @Override\n    public void dismount(DismountReason reason) {\n        this.owo$getWrapper().dismount(reason);\n    }\n\n    @Nullable\n    @Override\n    public ParentUIComponent parent() {\n        return this.owo$getWrapper().parent();\n    }\n\n    @Override\n    public @Nullable FocusHandler focusHandler() {\n        return this.owo$getWrapper().focusHandler();\n    }\n\n    @Override\n    public UIComponent positioning(Positioning positioning) {\n        this.owo$getWrapper().positioning(positioning);\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Positioning> positioning() {\n        return this.owo$getWrapper().positioning();\n    }\n\n    @Override\n    public UIComponent margins(Insets margins) {\n        this.owo$getWrapper().margins(margins);\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Insets> margins() {\n        return this.owo$getWrapper().margins();\n    }\n\n    @Override\n    public UIComponent horizontalSizing(Sizing horizontalSizing) {\n        this.owo$getWrapper().horizontalSizing(horizontalSizing);\n        return this;\n    }\n\n    @Override\n    public UIComponent verticalSizing(Sizing verticalSizing) {\n        this.owo$getWrapper().verticalSizing(verticalSizing);\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Sizing> horizontalSizing() {\n        return this.owo$getWrapper().horizontalSizing();\n    }\n\n    @Override\n    public AnimatableProperty<Sizing> verticalSizing() {\n        return this.owo$getWrapper().verticalSizing();\n    }\n\n    @Override\n    public EventSource<MouseDown> mouseDown() {\n        return this.owo$getWrapper().mouseDown();\n    }\n\n    @Override\n    public int x() {\n        return this.owo$getWrapper().x();\n    }\n\n    @Override\n    public int y() {\n        return this.owo$getWrapper().y();\n    }\n\n    @Override\n    public int width() {\n        return this.owo$getWrapper().width();\n    }\n\n    @Override\n    public int height() {\n        return this.owo$getWrapper().height();\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        this.owo$getWrapper().draw(graphics, mouseX, mouseY, partialTicks, delta);\n    }\n\n    @Override\n    public boolean shouldDrawTooltip(double mouseX, double mouseY) {\n        return this.owo$getWrapper().shouldDrawTooltip(mouseX, mouseY);\n    }\n\n    @Override\n    public void update(float delta, int mouseX, int mouseY) {\n        this.owo$getWrapper().update(delta, mouseX, mouseY);\n        this.cursorStyle(this.active ? this.owo$preferredCursorStyle() : CursorStyle.POINTER);\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        return this.owo$getWrapper().onMouseDown(click, doubled);\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        return this.owo$getWrapper().onMouseUp(click);\n    }\n\n    @Override\n    public EventSource<MouseUp> mouseUp() {\n        return this.owo$getWrapper().mouseUp();\n    }\n\n    @Override\n    public EventSource<MouseScroll> mouseScroll() {\n        return this.owo$getWrapper().mouseScroll();\n    }\n\n    @Override\n    public EventSource<MouseDrag> mouseDrag() {\n        return this.owo$getWrapper().mouseDrag();\n    }\n\n    @Override\n    public EventSource<KeyPress> keyPress() {\n        return this.owo$getWrapper().keyPress();\n    }\n\n    @Override\n    public EventSource<CharTyped> charTyped() {\n        return this.owo$getWrapper().charTyped();\n    }\n\n    @Override\n    public EventSource<FocusGained> focusGained() {\n        return this.owo$getWrapper().focusGained();\n    }\n\n    @Override\n    public EventSource<FocusLost> focusLost() {\n        return this.owo$getWrapper().focusLost();\n    }\n\n    @Override\n    public EventSource<MouseEnter> mouseEnter() {\n        return this.owo$getWrapper().mouseEnter();\n    }\n\n    @Override\n    public EventSource<MouseLeave> mouseLeave() {\n        return this.owo$getWrapper().mouseLeave();\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        return this.owo$getWrapper().onMouseScroll(mouseX, mouseY, amount);\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.owo$getWrapper().onMouseDrag(click, deltaX, deltaY);\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        return this.owo$getWrapper().onKeyPress(input);\n    }\n\n    @Override\n    public boolean onCharTyped(CharacterEvent input) {\n        return this.owo$getWrapper().onCharTyped(input);\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return true;\n    }\n\n    @Override\n    public void onFocusGained(FocusSource source) {\n        this.setFocused(source == FocusSource.KEYBOARD_CYCLE);\n        this.owo$getWrapper().onFocusGained(source);\n    }\n\n    @Override\n    public void onFocusLost() {\n        this.setFocused(false);\n        this.owo$getWrapper().onFocusLost();\n    }\n\n    @Override\n    public <C extends UIComponent> C configure(Consumer<C> closure) {\n        return this.owo$getWrapper().configure(closure);\n    }\n\n    @Override\n    public void parseProperties(UIModel spec, Element element, Map<String, Element> children) {\n        // --- copied from Component, because you can't invoke interface super methods in mixins - very cool ---\n\n        if (!element.getAttribute(\"id\").isBlank()) {\n            this.id(element.getAttribute(\"id\").strip());\n        }\n\n        UIParsing.apply(children, \"margins\", Insets::parse, this::margins);\n        UIParsing.apply(children, \"positioning\", Positioning::parse, this::positioning);\n        UIParsing.apply(children, \"cursor-style\", UIParsing.parseEnum(CursorStyle.class), this::cursorStyle);\n        UIParsing.apply(children, \"tooltip-text\", UIParsing::parseText, component -> this.tooltip(component));\n\n        if (children.containsKey(\"sizing\")) {\n            var sizingValues = UIParsing.childElements(children.get(\"sizing\"));\n            UIParsing.apply(sizingValues, \"vertical\", Sizing::parse, this::verticalSizing);\n            UIParsing.apply(sizingValues, \"horizontal\", Sizing::parse, this::horizontalSizing);\n        }\n\n        // --- end ---\n\n        UIParsing.apply(children, \"active\", UIParsing::parseBool, active -> this.active = active);\n    }\n\n    @Override\n    public CursorStyle cursorStyle() {\n        return this.owo$getWrapper().cursorStyle();\n    }\n\n    @Override\n    public UIComponent cursorStyle(CursorStyle style) {\n        return this.owo$getWrapper().cursorStyle(style);\n    }\n\n    @Override\n    public UIComponent tooltip(List<ClientTooltipComponent> tooltip) {\n        return this.owo$getWrapper().tooltip(tooltip);\n    }\n\n    @Override\n    public List<ClientTooltipComponent> tooltip() {\n        return this.owo$getWrapper().tooltip();\n    }\n\n    @Override\n    public UIComponent id(@Nullable String id) {\n        this.owo$getWrapper().id(id);\n        return this;\n    }\n\n    @Override\n    public @Nullable String id() {\n        return this.owo$getWrapper().id();\n    }\n\n    @Unique\n    protected VanillaWidgetComponent owo$getWrapper() {\n        if (this.wrapper == null) {\n            this.wrapper = UIComponents.wrapVanillaWidget((AbstractWidget) (Object) this);\n        }\n\n        return this.wrapper;\n    }\n\n    @Override\n    public @Nullable VanillaWidgetComponent widgetWrapper() {\n        return this.wrapper;\n    }\n\n    @Override\n    public int xOffset() {\n        return 0;\n    }\n\n    @Override\n    public int yOffset() {\n        return 0;\n    }\n\n    @Override\n    public int widthOffset() {\n        return 0;\n    }\n\n    @Override\n    public int heightOffset() {\n        return 0;\n    }\n\n    @Inject(method = \"setWidth\", at = @At(\"HEAD\"), cancellable = true)\n    private void applyWidthToWrapper(int width, CallbackInfo ci) {\n        var wrapper = this.wrapper;\n        if (wrapper != null) {\n            wrapper.horizontalSizing(Sizing.fixed(width));\n            ci.cancel();\n        }\n    }\n\n    @Override\n    public void updateX(int x) {\n        this.owo$getWrapper().updateX(x);\n    }\n\n    @Override\n    public void updateY(int y) {\n        this.owo$getWrapper().updateY(y);\n    }\n\n    protected CursorStyle owo$preferredCursorStyle() {\n        return CursorStyle.POINTER;\n    }\n\n    @Inject(method = \"render\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/components/AbstractWidget;renderWidget(Lnet/minecraft/client/gui/GuiGraphics;IIF)V\"))\n    private void setHovered(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) {\n        if (this.wrapper != null) this.isHovered = this.isHovered && this.wrapper.hovered();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/ChatScreenMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport io.wispforest.owo.ui.util.CommandOpenedScreen;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.ChatScreen;\nimport net.minecraft.client.input.KeyEvent;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(ChatScreen.class)\npublic class ChatScreenMixin {\n\n    @Inject(method = \"keyPressed\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/Minecraft;setScreen(Lnet/minecraft/client/gui/screens/Screen;)V\"), cancellable = true)\n    private void cancelClose(KeyEvent input, CallbackInfoReturnable<Boolean> cir) {\n        if (Minecraft.getInstance().screen instanceof CommandOpenedScreen) {\n            cir.setReturnValue(true);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/CubeMapMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport io.wispforest.owo.ui.renderstate.CubeMapElementRenderState;\nimport net.minecraft.client.renderer.CubeMap;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.ModifyArgs;\nimport org.spongepowered.asm.mixin.injection.invoke.arg.Args;\n\nimport java.util.OptionalInt;\n\n@Mixin(CubeMap.class)\npublic class CubeMapMixin {\n\n    @ModifyArgs(method = \"render\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/systems/CommandEncoder;createRenderPass(Ljava/util/function/Supplier;Lcom/mojang/blaze3d/textures/GpuTextureView;Ljava/util/OptionalInt;Lcom/mojang/blaze3d/textures/GpuTextureView;Ljava/util/OptionalDouble;)Lcom/mojang/blaze3d/systems/RenderPass;\"))\n    private void injectOutputTextures(Args args) {\n        if (CubeMapElementRenderState.outputOverride == null) return;\n\n        args.set(1, CubeMapElementRenderState.outputOverride.color());\n        args.set(2, OptionalInt.of(CubeMapElementRenderState.outputOverride.resetColor()));\n        args.set(3, CubeMapElementRenderState.outputOverride.depth());\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/EditBoxMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport io.wispforest.owo.mixin.ui.access.TextBoxComponentAccessor;\nimport io.wispforest.owo.ui.inject.GreedyInputUIComponent;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.network.chat.Component;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(EditBox.class)\npublic abstract class EditBoxMixin extends AbstractWidget implements GreedyInputUIComponent {\n\n    public EditBoxMixin(int x, int y, int width, int height, Component message) {\n        super(x, y, width, height, message);\n    }\n\n    @Inject(method = \"onValueChange\", at = @At(\"HEAD\"))\n    private void callOwoListener(String newText, CallbackInfo ci) {\n        if (!(this instanceof TextBoxComponentAccessor accessor)) return;\n        accessor.owo$textValue().set(newText);\n    }\n\n    @Override\n    public void onFocusGained(FocusSource source) {\n        super.onFocusGained(source);\n        this.setFocused(true);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/GuiRendererMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport com.llamalad7.mixinextras.injector.ModifyExpressionValue;\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.mojang.blaze3d.buffers.GpuBuffer;\nimport com.mojang.blaze3d.systems.RenderPass;\nimport com.mojang.blaze3d.systems.RenderSystem;\nimport com.mojang.blaze3d.textures.FilterMode;\nimport com.mojang.blaze3d.vertex.VertexFormat;\nimport io.wispforest.owo.mixin.ui.access.GlCommandEncoderAccessor;\nimport io.wispforest.owo.ui.renderstate.BlurQuadElementRenderState;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.render.GuiRenderer;\nimport net.minecraft.client.gui.render.TextureSetup;\nimport net.minecraft.client.gui.render.state.GuiElementRenderState;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Vector2i;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.ModifyArgs;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.spongepowered.asm.mixin.injection.invoke.arg.Args;\n\n@Mixin(GuiRenderer.class)\npublic class GuiRendererMixin {\n\n    @Shadow\n    @Nullable\n    private TextureSetup previousTextureSetup;\n\n    @ModifyArgs(\n        method = \"executeDraw(Lnet/minecraft/client/gui/render/GuiRenderer$Draw;Lcom/mojang/blaze3d/systems/RenderPass;Lcom/mojang/blaze3d/buffers/GpuBuffer;Lcom/mojang/blaze3d/vertex/VertexFormat$IndexType;)V\",\n        at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/systems/RenderPass;setIndexBuffer(Lcom/mojang/blaze3d/buffers/GpuBuffer;Lcom/mojang/blaze3d/vertex/VertexFormat$IndexType;)V\")\n    )\n    private void fixNonQuadIndexing(Args args, @Local(argsOnly = true) GuiRenderer.Draw draw) {\n        var pipeline = draw.pipeline();\n        if (!pipeline.getLocation().getNamespace().equals(\"owo\")) return;\n\n        if (pipeline.getVertexFormatMode() != VertexFormat.Mode.QUADS) {\n            var shapeIndexBuffer = RenderSystem.getSequentialBuffer(pipeline.getVertexFormatMode());\n            args.set(0, shapeIndexBuffer.getBuffer(draw.indexCount()));\n            args.set(1, shapeIndexBuffer.type());\n        }\n    }\n\n    @Inject(\n        method = \"executeDraw(Lnet/minecraft/client/gui/render/GuiRenderer$Draw;Lcom/mojang/blaze3d/systems/RenderPass;Lcom/mojang/blaze3d/buffers/GpuBuffer;Lcom/mojang/blaze3d/vertex/VertexFormat$IndexType;)V\",\n        at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/systems/RenderPass;drawIndexed(IIII)V\")\n    )\n    private void drawBlur(GuiRenderer.Draw draw, RenderPass pass, GpuBuffer indexBuffer, VertexFormat.IndexType indexType, CallbackInfo ci) {\n        var blurSetup = BlurQuadElementRenderState.getBlurSetupOf(draw.textureSetup());\n        if (blurSetup == null) return;\n\n        var mainBuffer = Minecraft.getInstance().getMainRenderTarget();\n        var inputSize = new Vector2i(mainBuffer.width, mainBuffer.height);\n\n        var encoder = RenderSystem.getDevice().createCommandEncoder();\n\n        ((GlCommandEncoderAccessor)encoder).owo$setInRenderPass(false);\n        encoder.copyTextureToTexture(\n            Minecraft.getInstance().getMainRenderTarget().getColorTexture(),\n            BlurQuadElementRenderState.input.getColorTexture(),\n            0, 0, 0, 0, 0, inputSize.x, inputSize.y\n        );\n\n        var uniforms = BlurQuadElementRenderState.uniforms.write(inputSize, blurSetup.directions(), blurSetup.quality(), blurSetup.size());\n        ((GlCommandEncoderAccessor)encoder).owo$setInRenderPass(true);\n\n        pass.setUniform(\"BlurSettings\", uniforms);\n        pass.bindTexture(\"InputSampler\", BlurQuadElementRenderState.inputView, RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));\n    }\n\n    @ModifyExpressionValue(method = \"addElementToMesh\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/render/TextureSetup;equals(Ljava/lang/Object;)Z\"))\n    private boolean adjustCheckForBlurElements(boolean original, @Local(argsOnly = true) GuiElementRenderState state) {\n        return original && !(state instanceof BlurQuadElementRenderState || BlurQuadElementRenderState.hasBlurSetupFor(previousTextureSetup));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/MinecraftMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport com.mojang.blaze3d.platform.Window;\nimport io.wispforest.owo.ui.event.ClientRenderCallback;\nimport io.wispforest.owo.ui.event.WindowResizeCallback;\nimport io.wispforest.owo.ui.renderstate.BlurQuadElementRenderState;\nimport io.wispforest.owo.ui.util.DisposableScreen;\nimport net.minecraft.CrashReport;\nimport net.minecraft.ReportedException;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.main.GameConfig;\nimport org.jetbrains.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\n@Mixin(Minecraft.class)\npublic class MinecraftMixin {\n\n    @Unique\n    private final Set<DisposableScreen> screensToDispose = new HashSet<>();\n\n    @Shadow\n    @Final\n    private Window window;\n\n    @Shadow\n    @Nullable\n    public Screen screen;\n\n    @Inject(method = \"resizeDisplay\", at = @At(\"TAIL\"))\n    private void captureResize(CallbackInfo ci) {\n        WindowResizeCallback.EVENT.invoker().onResized((Minecraft) (Object) this, this.window);\n    }\n\n    @Inject(method = \"runTick\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/platform/Window;setErrorSection(Ljava/lang/String;)V\", ordinal = 1))\n    private void beforeRender(boolean tick, CallbackInfo ci) {\n        ClientRenderCallback.BEFORE.invoker().onRender((Minecraft) (Object) this);\n    }\n\n    @Inject(method = \"runTick\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/platform/Window;updateDisplay(Lcom/mojang/blaze3d/TracyFrameCapture;)V\", shift = At.Shift.AFTER))\n    private void afterRender(boolean tick, CallbackInfo ci) {\n        ClientRenderCallback.AFTER.invoker().onRender((Minecraft) (Object) this);\n    }\n\n    @Inject(method = \"runTick\", at = @At(value = \"FIELD\", target = \"Lnet/minecraft/client/Minecraft;frameTimeNs:J\"))\n    private void beforeSwap(boolean tick, CallbackInfo ci) {\n        ClientRenderCallback.BEFORE_SWAP.invoker().onRender((Minecraft) (Object) this);\n    }\n\n    @Inject(method = \"setScreen\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/screens/Screen;removed()V\"))\n    private void captureSetScreen(Screen screen, CallbackInfo ci) {\n        if (screen != null && this.screen instanceof DisposableScreen disposable) {\n            this.screensToDispose.add(disposable);\n        } else if (screen == null) {\n            if (this.screen instanceof DisposableScreen disposable) {\n                this.screensToDispose.add(disposable);\n            }\n\n            for (var disposable : this.screensToDispose) {\n                try {\n                    disposable.dispose();\n                } catch (Throwable error) {\n                    var report = new CrashReport(\"Failed to dispose screen\", error);\n                    report.addCategory(\"Screen being disposed: \")\n                            .setDetail(\"Screen class\", disposable.getClass())\n                            .setDetail(\"Screen being closed\", this.screen)\n                            .setDetail(\"Total screens to dispose\", this.screensToDispose.size());\n\n                    throw new ReportedException(report);\n                }\n            }\n\n            this.screensToDispose.clear();\n        }\n    }\n\n    @Inject(method = \"<init>\", at = @At(value = \"INVOKE\", target = \"Lcom/mojang/blaze3d/systems/RenderSystem;initRenderer(JIZLcom/mojang/blaze3d/shaders/ShaderSource;Z)V\", shift = At.Shift.AFTER))\n    private void initBlurRenderer(GameConfig args, CallbackInfo ci) {\n        BlurQuadElementRenderState.initialize((Minecraft) (Object) this);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/MultiLineEditBoxMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.ui.inject.GreedyInputUIComponent;\nimport net.minecraft.client.gui.components.AbstractScrollArea;\nimport net.minecraft.client.gui.components.MultiLineEditBox;\nimport net.minecraft.network.chat.Component;\nimport org.spongepowered.asm.mixin.Mixin;\n\n@Mixin(MultiLineEditBox.class)\npublic abstract class MultiLineEditBoxMixin extends AbstractScrollArea implements GreedyInputUIComponent {\n\n    public MultiLineEditBoxMixin(int i, int j, int k, int l, Component text) {\n        super(i, j, k, l, text);\n    }\n\n    @Override\n    public void onFocusGained(UIComponent.FocusSource source) {\n        super.onFocusGained(source);\n        this.setFocused(true);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/ScreenMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport com.llamalad7.mixinextras.injector.ModifyExpressionValue;\nimport io.wispforest.owo.ui.base.BaseOwoContainerScreen;\nimport io.wispforest.owo.ui.base.BaseOwoScreen;\nimport net.minecraft.client.gui.screens.Screen;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\n@Mixin(Screen.class)\npublic class ScreenMixin {\n\n    @ModifyExpressionValue(method = \"keyPressed\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/screens/Screen;shouldCloseOnEsc()Z\", ordinal = 0))\n    private boolean dontCloseOwoScreens(boolean original) {\n        //noinspection ConstantValue\n        if ((Object) this instanceof BaseOwoScreen<?> || (Object) this instanceof BaseOwoContainerScreen<?, ?>) {\n            return false;\n        }\n\n        return original;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/SlotAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport net.minecraft.world.inventory.Slot;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(Slot.class)\npublic interface SlotAccessor {\n    @Mutable\n    @Accessor(\"x\")\n    void owo$setX(int x);\n\n    @Mutable\n    @Accessor(\"y\")\n    void owo$setY(int y);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/SlotMixin.java",
    "content": "package io.wispforest.owo.mixin.ui;\n\nimport io.wispforest.owo.ui.core.PositionedRectangle;\nimport io.wispforest.owo.util.pond.OwoSlotExtension;\nimport net.minecraft.world.inventory.Slot;\nimport org.jetbrains.annotations.Nullable;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(Slot.class)\npublic class SlotMixin implements OwoSlotExtension {\n\n    @Unique\n    private boolean disabledOverride = false;\n\n    @Unique\n    private @Nullable PositionedRectangle scissorArea = null;\n\n    @Override\n    public void owo$setDisabledOverride(boolean disabled) {\n        this.disabledOverride = disabled;\n    }\n\n    @Override\n    public boolean owo$getDisabledOverride() {\n        return this.disabledOverride;\n    }\n\n    @Override\n    public void owo$setScissorArea(@Nullable PositionedRectangle scissor) {\n        this.scissorArea = scissor;\n    }\n\n    @Override\n    public @Nullable PositionedRectangle owo$getScissorArea() {\n        return this.scissorArea;\n    }\n\n    @Inject(method = \"isActive\", at = @At(\"TAIL\"), cancellable = true)\n    private void injectOverride(CallbackInfoReturnable<Boolean> cir) {\n        if (!this.disabledOverride) return;\n        cir.setReturnValue(false);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/AbstractWidgetAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.components.WidgetTooltipHolder;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(AbstractWidget.class)\npublic interface AbstractWidgetAccessor {\n\n    @Accessor(\"height\")\n    void owo$setHeight(int height);\n\n    @Accessor(\"width\")\n    void owo$setWidth(int width);\n\n    @Accessor(\"x\")\n    void owo$setX(int x);\n\n    @Accessor(\"y\")\n    void owo$setY(int y);\n\n    @Accessor(\"tooltip\")\n    WidgetTooltipHolder owo$getTooltip();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/BaseOwoHandledScreenAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport io.wispforest.owo.ui.base.BaseOwoContainerScreen;\nimport io.wispforest.owo.ui.core.OwoUIAdapter;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(value = BaseOwoContainerScreen.class, remap = false)\npublic interface BaseOwoHandledScreenAccessor {\n    @Accessor(\"uiAdapter\")\n    OwoUIAdapter<?> owo$getUIAdapter();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/BlockEntityAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.state.BlockState;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(BlockEntity.class)\npublic interface BlockEntityAccessor {\n    @Accessor(\"blockState\")\n    void owo$setBlockState(BlockState state);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/ButtonAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.gui.components.Button;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(Button.class)\npublic interface ButtonAccessor {\n\n    @Mutable\n    @Accessor(\"onPress\")\n    void owo$setOnPress(Button.OnPress onPress);\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/CheckboxAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.gui.components.Checkbox;\nimport net.minecraft.client.gui.components.MultiLineTextWidget;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(Checkbox.class)\npublic interface CheckboxAccessor {\n    @Accessor(\"selected\")\n    void owo$setSelected(boolean checked);\n\n    @Accessor(\"textWidget\")\n    MultiLineTextWidget owo$getTextWidget();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/EditBoxAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.gui.components.EditBox;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(EditBox.class)\npublic interface EditBoxAccessor {\n    @Accessor(\"bordered\")\n    boolean owo$bordered();\n\n    @Invoker(\"updateTextPosition\")\n    void owo$updateTextPosition();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/EntityRendererAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.renderer.entity.EntityRenderer;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.entity.Entity;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\n@Mixin(EntityRenderer.class)\npublic interface EntityRendererAccessor<T extends Entity> {\n    @Invoker(\"getNameTag\")\n    Component owo$getNameTag(T entity);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/GlCommandEncoderAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport com.mojang.blaze3d.opengl.GlCommandEncoder;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(GlCommandEncoder.class)\npublic interface GlCommandEncoderAccessor {\n\n    @Accessor(\"inRenderPass\")\n    void owo$setInRenderPass(boolean open);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/GuiGraphicsAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2fStack;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\nimport org.spongepowered.asm.mixin.gen.Invoker;\n\nimport java.util.List;\n\n@Mixin(GuiGraphics.class)\npublic interface GuiGraphicsAccessor {\n\n    @Invoker(\"renderTooltip\")\n    void owo$drawTooltipImmediately(Font textRenderer, List<ClientTooltipComponent> components, int x, int y, ClientTooltipPositioner positioner, @Nullable Identifier texture);\n\n    @Accessor(\"pose\")\n    Matrix3x2fStack owo$getPose();\n\n    @Mutable\n    @Accessor(\"pose\")\n    void owo$setPose(Matrix3x2fStack matrices);\n\n    @Accessor(\"scissorStack\")\n    GuiGraphics.ScissorStack owo$getScissorStack();\n\n    @Mutable\n    @Accessor(\"scissorStack\")\n    void owo$setScissorStack(GuiGraphics.ScissorStack scissorStack);\n\n    @Accessor(\"deferredTooltip\")\n    void owo$setDeferredTooltip(Runnable drawer);\n\n    @Accessor(\"deferredTooltip\")\n    Runnable owo$getDeferredTooltip();\n\n    @Accessor(\"mouseX\")\n    int owo$getMouseX();\n\n    @Accessor(\"mouseY\")\n    int owo$getMouseY();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/MultiLineEditBoxAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.gui.components.MultiLineEditBox;\nimport net.minecraft.client.gui.components.MultilineTextField;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(MultiLineEditBox.class)\npublic interface MultiLineEditBoxAccessor {\n\n    @Accessor(\"textField\")\n    MultilineTextField owo$getTextField();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/MultilineTextFieldAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport net.minecraft.client.gui.components.MultilineTextField;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Mutable;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(MultilineTextField.class)\npublic interface MultilineTextFieldAccessor {\n\n    @Mutable\n    @Accessor(\"width\")\n    void owo$setWidth(int width);\n\n    @Accessor(\"selectCursor\")\n    void owo$setSelectCursor(int width);\n\n    @Accessor(\"selectCursor\")\n    int owo$getSelectCursor();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/RenderSystemAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport com.mojang.blaze3d.buffers.GpuBufferSlice;\nimport com.mojang.blaze3d.systems.RenderSystem;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(RenderSystem.class)\npublic interface RenderSystemAccessor {\n    @Accessor(\"shaderLightDirections\")\n    static GpuBufferSlice owo$getShaderLightDirections() {\n        throw new UnsupportedOperationException();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/access/TextBoxComponentAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.access;\n\nimport io.wispforest.owo.ui.component.TextBoxComponent;\nimport io.wispforest.owo.util.Observable;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n// now you might think that simply AW'ing onChanged in TextFieldWidget\n// would be the way to go about this. but you see, tiny remapper (or more specifically how\n// loom uses it) begs to differ and simply does not remap your override, causing\n// that approach to break in prod. thus we need to mix into TextFieldWidget\n// and use this accessor to update it instead\n@ApiStatus.Internal\n@Mixin(TextBoxComponent.class)\npublic interface TextBoxComponentAccessor {\n\n    @Accessor(\"textValue\")\n    Observable<String> owo$textValue();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/display/GameRendererMixin.java",
    "content": "package io.wispforest.owo.mixin.ui.display;\n\nimport com.llamalad7.mixinextras.sugar.Local;\nimport com.llamalad7.mixinextras.sugar.Share;\nimport com.llamalad7.mixinextras.sugar.ref.LocalRef;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.events.MouseButtonReleaseEvent;\nimport io.wispforest.owo.braid.core.events.MouseMoveEvent;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport net.minecraft.client.DeltaTracker;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.GameRenderer;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.phys.BlockHitResult;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.Unique;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(GameRenderer.class)\npublic class GameRendererMixin {\n\n    @Shadow\n    @Final\n    private Minecraft minecraft;\n\n    @Inject(method = \"render\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/renderer/GameRenderer;renderLevel(Lnet/minecraft/client/DeltaTracker;)V\"))\n    public void beforeWorldRender(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) {\n        BraidDisplayBinding.updateAndDrawDisplays();\n    }\n\n    @Inject(method = \"pick\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/player/LocalPlayer;raycastHitResult(FLnet/minecraft/world/entity/Entity;)Lnet/minecraft/world/phys/HitResult;\"))\n    public void updateTargetDisplay(\n        float tickDelta,\n        CallbackInfo ci,\n        @Local Entity camera,\n        @Share(\"camera\") LocalRef<Entity> cameraRef,\n        @Share(\"target_display\") LocalRef<BraidDisplayBinding.DisplayHitResult> targetDisplay\n    ) {\n        cameraRef.set(camera);\n        targetDisplay.set(\n            BraidDisplayBinding.queryTargetDisplay(camera.getEyePosition(tickDelta), camera.getViewVector(tickDelta))\n        );\n    }\n\n    @Inject(method = \"pick\", at = @At(value = \"TAIL\"))\n    public void checkDisplayHitTest(\n        float tickDelta,\n        CallbackInfo ci,\n        @Share(\"camera\") LocalRef<Entity> cameraRef,\n        @Share(\"target_display\") LocalRef<BraidDisplayBinding.DisplayHitResult> targetDisplay\n    ) {\n        if (targetDisplay.get() == null) {\n            this.setTargetDisplay(null);\n            return;\n        }\n\n        var displayHitPoint = targetDisplay.get().display().quad.unproject(targetDisplay.get().point());\n\n        var cameraPos = cameraRef.get().getEyePosition(tickDelta);\n        if (this.minecraft.hitResult.getLocation().distanceToSqr(cameraPos) > displayHitPoint.distanceToSqr(cameraPos)) {\n            this.setTargetDisplay(targetDisplay.get());\n            BraidDisplayBinding.onDisplayHit(BraidDisplayBinding.targetDisplay);\n\n            var display = BraidDisplayBinding.targetDisplay.display();\n\n            if (display.primaryPressed && !Minecraft.getInstance().options.keyUse.isDown()) {\n                display.app.eventBinding.add(new MouseButtonReleaseEvent(GLFW.GLFW_MOUSE_BUTTON_LEFT, KeyModifiers.NONE));\n                display.primaryPressed = false;\n            }\n\n            if (display.secondaryPressed && !Minecraft.getInstance().options.keyAttack.isDown()) {\n                display.app.eventBinding.add(new MouseButtonReleaseEvent(GLFW.GLFW_MOUSE_BUTTON_RIGHT, KeyModifiers.NONE));\n                display.secondaryPressed = false;\n            }\n\n            this.minecraft.hitResult = BlockHitResult.miss(\n                this.minecraft.hitResult.getLocation(),\n                Direction.UP,\n                BlockPos.containing(this.minecraft.hitResult.getLocation())\n            );\n\n            this.minecraft.crosshairPickEntity = null;\n        } else {\n            this.setTargetDisplay(null);\n        }\n    }\n\n    @Unique\n    private void setTargetDisplay(@Nullable BraidDisplayBinding.DisplayHitResult newTarget) {\n        if (BraidDisplayBinding.targetDisplay == null) {\n            BraidDisplayBinding.targetDisplay = newTarget;\n            return;\n        }\n\n        if (newTarget == null || BraidDisplayBinding.targetDisplay.display() != newTarget.display()) {\n            BraidDisplayBinding.targetDisplay.display().app.eventBinding.add(new MouseMoveEvent(0, 0));\n        }\n\n        BraidDisplayBinding.targetDisplay = newTarget;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/display/GuiMixin.java",
    "content": "package io.wispforest.owo.mixin.ui.display;\n\nimport com.llamalad7.mixinextras.injector.ModifyExpressionValue;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.braid.core.cursor.SystemCursorStyle;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport net.minecraft.client.gui.Gui;\nimport net.minecraft.resources.Identifier;\nimport org.lwjgl.glfw.GLFW;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\n@Mixin(Gui.class)\npublic class GuiMixin {\n\n    @ModifyExpressionValue(method = \"renderCrosshair\", at = @At(value = \"FIELD\", target = \"Lnet/minecraft/client/gui/Gui;CROSSHAIR_SPRITE:Lnet/minecraft/resources/Identifier;\"))\n    private Identifier injectDisplayCrosshair(Identifier original) {\n        if (BraidDisplayBinding.targetDisplay == null) return original;\n\n        var cursorStyle = BraidDisplayBinding.targetDisplay.display().app.surface.currentCursorStyle();\n        if (!(cursorStyle instanceof SystemCursorStyle systemStyle)) return original;\n\n        return switch (systemStyle.glfwId) {\n            case GLFW.GLFW_RESIZE_NESW_CURSOR -> Owo.id(\"cursors/nesw_resize\");\n            case GLFW.GLFW_RESIZE_NWSE_CURSOR -> Owo.id(\"cursors/nwse_resize\");\n            case GLFW.GLFW_VRESIZE_CURSOR -> Owo.id(\"cursors/vertical_resize\");\n            case GLFW.GLFW_HRESIZE_CURSOR -> Owo.id(\"cursors/horizontal_resize\");\n            case GLFW.GLFW_RESIZE_ALL_CURSOR -> Owo.id(\"cursors/all_resize\");\n            case GLFW.GLFW_CROSSHAIR_CURSOR -> Owo.id(\"cursors/crosshair\");\n            case GLFW.GLFW_HAND_CURSOR -> Owo.id(\"cursors/hand\");\n            case 0 -> Owo.id(\"cursors/none\");\n            default -> original;\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/display/MinecraftMixin.java",
    "content": "package io.wispforest.owo.mixin.ui.display;\n\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.events.MouseButtonPressEvent;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.player.LocalPlayer;\nimport net.minecraft.world.InteractionHand;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;\n\n@Mixin(Minecraft.class)\npublic class MinecraftMixin {\n\n    @Shadow\n    @Nullable\n    public LocalPlayer player;\n\n    @Inject(method = \"startUseItem\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/player/LocalPlayer;isHandsBusy()Z\"), cancellable = true)\n    public void dispatchSecondaryPressEvent(CallbackInfo ci) {\n        if (BraidDisplayBinding.targetDisplay == null || BraidDisplayBinding.targetDisplay.display().primaryPressed) return;\n\n        var eventBinding = BraidDisplayBinding.targetDisplay.display().app.eventBinding;\n        eventBinding.add(new MouseButtonPressEvent(GLFW.GLFW_MOUSE_BUTTON_LEFT, KeyModifiers.NONE));\n\n        BraidDisplayBinding.targetDisplay.display().primaryPressed = true;\n        this.player.swing(InteractionHand.MAIN_HAND);\n\n        ci.cancel();\n    }\n\n    @Inject(method = \"startAttack\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/player/LocalPlayer;getItemInHand(Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/item/ItemStack;\"), cancellable = true)\n    public void dispatchPrimaryPressEvent(CallbackInfoReturnable<Boolean> cir) {\n        if (BraidDisplayBinding.targetDisplay == null || BraidDisplayBinding.targetDisplay.display().secondaryPressed) return;\n\n        var eventBinding = BraidDisplayBinding.targetDisplay.display().app.eventBinding;\n        eventBinding.add(new MouseButtonPressEvent(GLFW.GLFW_MOUSE_BUTTON_RIGHT, KeyModifiers.NONE));\n\n        BraidDisplayBinding.targetDisplay.display().secondaryPressed = true;\n        this.player.swing(InteractionHand.MAIN_HAND);\n\n        cir.setReturnValue(true);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/display/MouseHandlerMixin.java",
    "content": "package io.wispforest.owo.mixin.ui.display;\n\nimport com.llamalad7.mixinextras.sugar.Local;\nimport io.wispforest.owo.braid.core.events.MouseScrollEvent;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.MouseHandler;\nimport org.spongepowered.asm.mixin.Final;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(MouseHandler.class)\npublic class MouseHandlerMixin {\n\n    @Shadow\n    @Final\n    private Minecraft minecraft;\n\n    @Inject(method = \"onScroll\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/player/LocalPlayer;getInventory()Lnet/minecraft/world/entity/player/Inventory;\"), cancellable = true)\n    public void scrollBraidDisplays(long window, double horizontal, double vertical, CallbackInfo ci, @Local(ordinal = 3) double xOffset, @Local(ordinal = 4) double yOffset) {\n        if (BraidDisplayBinding.targetDisplay == null || this.minecraft.player.isShiftKeyDown()) return;\n\n        BraidDisplayBinding.targetDisplay.display().app.eventBinding.add(new MouseScrollEvent(xOffset, yOffset));\n        ci.cancel();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/layers/AbstractContainerScreenAccessor.java",
    "content": "package io.wispforest.owo.mixin.ui.layers;\n\nimport net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.gen.Accessor;\n\n@Mixin(AbstractContainerScreen.class)\npublic interface AbstractContainerScreenAccessor {\n\n    @Accessor(\"leftPos\")\n    int owo$getRootX();\n\n    @Accessor(\"topPos\")\n    int owo$getRootY();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/layers/KeyboardHandlerMixin.java",
    "content": "package io.wispforest.owo.mixin.ui.layers;\n\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport io.wispforest.owo.ui.layers.Layers;\nimport net.minecraft.client.KeyboardHandler;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.input.CharacterEvent;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\n@Mixin(KeyboardHandler.class)\npublic class KeyboardHandlerMixin {\n\n    @WrapOperation(method = \"charTyped\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/screens/Screen;charTyped(Lnet/minecraft/client/input/CharacterEvent;)Z\"))\n    private boolean captureScreenCharTyped(Screen screen, CharacterEvent charInput, Operation<Boolean> original) {\n        boolean handled = false;\n        for (var instance : Layers.getInstances(screen)) {\n            handled = instance.adapter.charTyped(charInput);\n            if (handled) break;\n        }\n\n        return handled || original.call(screen, charInput);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/layers/MouseHandlerMixin.java",
    "content": "package io.wispforest.owo.mixin.ui.layers;\n\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport io.wispforest.owo.ui.layers.Layers;\nimport net.minecraft.client.MouseHandler;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\n@Mixin(MouseHandler.class)\npublic class MouseHandlerMixin {\n\n    @WrapOperation(method = \"handleAccumulatedMovement\", at = @At(value = \"INVOKE\", target = \"Lnet/minecraft/client/gui/screens/Screen;mouseDragged(Lnet/minecraft/client/input/MouseButtonEvent;DD)Z\"))\n    private boolean captureScreenMouseDrag(Screen screen, MouseButtonEvent click, double deltaX, double deltaY, Operation<Boolean> original) {\n        boolean handled = false;\n        for (var instance : Layers.getInstances(screen)) {\n            handled = instance.adapter.mouseDragged(click, deltaX, deltaY);\n            if (handled) break;\n        }\n\n        return handled || original.call(screen, click, deltaX, deltaY);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/mixin/ui/layers/ScreenMixin.java",
    "content": "package io.wispforest.owo.mixin.ui.layers;\n\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.layers.Layer;\nimport io.wispforest.owo.ui.layers.Layers;\nimport io.wispforest.owo.util.pond.OwoScreenExtension;\nimport net.minecraft.client.gui.components.events.AbstractContainerEventHandler;\nimport net.minecraft.client.gui.screens.Screen;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.Shadow;\n\nimport java.util.*;\n\n@Mixin(value = Screen.class, priority = 1100)\npublic abstract class ScreenMixin extends AbstractContainerEventHandler implements OwoScreenExtension {\n\n    @Shadow public int width;\n    @Shadow public int height;\n\n    private final List<Layer<?, ?>.Instance> owo$instances = new ArrayList<>();\n    private final List<Layer<?, ?>.Instance> owo$instancesView = Collections.unmodifiableList(this.owo$instances);\n    private final Map<Layer<?, ?>, Layer<?, ?>.Instance> owo$layersToInstances = new HashMap<>();\n\n    private boolean owo$layersInitialized = false;\n\n    @SuppressWarnings(\"ConstantConditions\")\n    private Screen owo$this() {\n        return (Screen) (Object) this;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void owo$updateLayers() {\n        if (this.owo$layersInitialized) {\n            for (var instance : this.owo$instances) {\n                instance.resize(this.width, this.height);\n            }\n        } else {\n            for (var layer : Layers.getLayers((Class<Screen>) this.owo$this().getClass())) {\n                var instance = layer.instantiate(this.owo$this());\n                this.owo$instances.add(instance);\n                this.owo$layersToInstances.put(layer, instance);\n\n                instance.adapter.inflateAndMount();\n            }\n\n            this.owo$layersInitialized = true;\n        }\n\n        this.owo$instances.forEach(Layer.Instance::dispatchLayoutUpdates);\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public <S extends Screen, R extends ParentUIComponent> Layer<S, R>.Instance owo$getInstance(Layer<S, R> layer) {\n        return (Layer<S, R>.Instance) this.owo$layersToInstances.get(layer);\n    }\n\n    @Override\n    public List<Layer<?, ?>.Instance> owo$getInstancesView() {\n        return this.owo$instancesView;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/moddata/ModDataConsumer.java",
    "content": "package io.wispforest.owo.moddata;\n\nimport com.google.gson.JsonObject;\nimport net.minecraft.resources.Identifier;\n\n/**\n * A class that can accept some JSON data loaded from a subdirectory\n * of all other mods' {@code data} directories when instructed to using\n * {@link ModDataLoader#load(ModDataConsumer)}\n */\npublic interface ModDataConsumer {\n\n    /**\n     * The {@code data} subdirectory to search. For example {@code items} would\n     * mean {@code .../data/{modid}/items/...}\n     *\n     * @return The subdirectory to load from\n     */\n    String getDataSubdirectory();\n\n    /**\n     * This method should process the loaded data\n     *\n     * @param object The .json files parsed into {@code JsonObject}s\n     */\n    void acceptParsedFile(Identifier id, JsonObject object);\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/moddata/ModDataLoader.java",
    "content": "package io.wispforest.owo.moddata;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonObject;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.util.DataExtensionUtil;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.resources.Identifier;\nimport org.apache.commons.io.FilenameUtils;\n\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Contains the logic to load JSON from all other mods' data directories\n * when {@link #load(ModDataConsumer)} is called. This should ideally be done\n * one and in a {@link net.fabricmc.api.ModInitializer}\n */\npublic final class ModDataLoader {\n\n    private static final Gson GSON = new Gson();\n\n    private static final Path DATA_PATH = FabricLoader.getInstance().getGameDir().resolve(\"moddata\");\n\n    private ModDataLoader() {}\n\n    /**\n     * Loads the data the {@code consumer} requests\n     *\n     * @param consumer The consumer to load data for\n     */\n    public static void load(ModDataConsumer consumer) {\n        Map<Identifier, JsonObject> foundFiles = new HashMap<>();\n\n        FabricLoader.getInstance().getAllMods().forEach(modContainer -> {\n            for (var rootPath : modContainer.getRootPaths()) {\n                final var targetPath = rootPath.resolve(String.format(\"data/%s/%s\", modContainer.getMetadata().getId(), consumer.getDataSubdirectory()));\n                tryLoadFilesFrom(foundFiles, modContainer.getMetadata().getId(), targetPath);\n            }\n        });\n\n        try {\n            Files.createDirectories(DATA_PATH);\n\n            try (var stream = Files.list(DATA_PATH)) {\n                stream.forEach(nsPath -> {\n                    if (!Files.isDirectory(nsPath)) return;\n\n                    var namespace = nsPath.getFileName().toString();\n                    var targetPath = nsPath.resolve(consumer.getDataSubdirectory());\n                    if (!Files.exists(targetPath)) return;\n\n                    tryLoadFilesFrom(foundFiles, namespace, targetPath);\n                });\n            }\n        } catch (IOException e) {\n            Owo.LOGGER.error(\"### Unable to traverse global data tree ++ Stacktrace below ###\", e);\n        }\n\n        foundFiles.forEach(consumer::acceptParsedFile);\n    }\n\n    private static void tryLoadFilesFrom(Map<Identifier, JsonObject> foundFiles, String namespace, Path targetPath) {\n        try {\n            if (!Files.exists(targetPath)) return;\n\n            try (var stream = Files.walk(targetPath)) {\n                stream.forEach(path -> {\n                    if (!path.toString().endsWith(\".json\") && !path.toString().endsWith(\".json5\")) return;\n                    try {\n                        final InputStreamReader tabData = new InputStreamReader(DataExtensionUtil.coerceJson(Files.newInputStream(path)) );\n\n                        foundFiles.put(Identifier.fromNamespaceAndPath(namespace, FilenameUtils.removeExtension(targetPath.relativize(path).toString())), GSON.fromJson(tabData, JsonObject.class));\n                    } catch (IOException e) {\n                        Owo.LOGGER.warn(\"### Unable to open data file {} ++ Stacktrace below ###\", path, e);\n                    }\n                });\n            }\n\n        } catch (IOException e) {\n            Owo.LOGGER.error(\"### Unable to traverse data tree {} ++ Stacktrace below ###\", targetPath, e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/network/ClientAccess.java",
    "content": "package io.wispforest.owo.network;\n\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.multiplayer.ClientPacketListener;\nimport net.minecraft.client.player.LocalPlayer;\n\npublic class ClientAccess implements OwoNetChannel.EnvironmentAccess<LocalPlayer, Minecraft, ClientPacketListener> {\n\n    @Environment(EnvType.CLIENT) private final ClientPacketListener packetListener;\n    @Environment(EnvType.CLIENT) private final Minecraft instance = Minecraft.getInstance();\n\n    public ClientAccess(ClientPacketListener packetListener) {\n        this.packetListener = packetListener;\n    }\n\n    @Override\n    @Environment(EnvType.CLIENT)\n    public LocalPlayer player() {\n        return instance.player;\n    }\n\n    @Override\n    @Environment(EnvType.CLIENT)\n    public Minecraft runtime() {\n        return instance;\n    }\n\n    @Override\n    @Environment(EnvType.CLIENT)\n    public ClientPacketListener packetListener() {\n        return packetListener;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/network/NetworkException.java",
    "content": "package io.wispforest.owo.network;\n\npublic class NetworkException extends IllegalStateException {\n\n    public NetworkException(String cause) {\n        super(cause);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/network/OwoClientConnectionExtension.java",
    "content": "package io.wispforest.owo.network;\n\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.Set;\n\n@ApiStatus.Internal\npublic interface OwoClientConnectionExtension {\n    void owo$setChannelSet(Set<Identifier> channels);\n\n    Set<Identifier> owo$getChannelSet();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/network/OwoHandshake.java",
    "content": "package io.wispforest.owo.network;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.ClientCommonPacketListenerImplAccessor;\nimport io.wispforest.owo.mixin.ServerCommonPacketListenerImplAccessor;\nimport io.wispforest.owo.ops.TextOps;\nimport io.wispforest.owo.particles.systems.ParticleSystemController;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.util.OwoFreezer;\nimport io.wispforest.owo.util.ServicesFrozenException;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworking;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;\nimport net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;\nimport net.fabricmc.fabric.api.networking.v1.ServerConfigurationConnectionEvents;\nimport net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.multiplayer.ClientConfigurationPacketListenerImpl;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.MinecraftServer;\nimport net.minecraft.server.network.ServerConfigurationPacketListenerImpl;\nimport net.minecraft.util.Tuple;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.ToIntFunction;\n\n@ApiStatus.Internal\npublic final class OwoHandshake {\n\n    private static final Endec<Map<Identifier, Integer>> CHANNEL_HASHES_ENDEC = Endec.map(MinecraftEndecs.IDENTIFIER, Endec.INT);\n\n    private static final MutableComponent PREFIX = TextOps.concat(Owo.PREFIX, Component.nullToEmpty(\"§chandshake failure\\n\"));\n    public static final Identifier CHANNEL_ID = Owo.id(\"handshake\");\n    public static final Identifier OFF_CHANNEL_ID = Owo.id(\"handshake_off\");\n\n    private static final boolean ENABLED = System.getProperty(\"owo.handshake.enabled\") != null ? Boolean.getBoolean(\"owo.handshake.enabled\") : Owo.DEBUG;\n    private static boolean HANDSHAKE_REQUIRED = false;\n    private static boolean QUERY_RECEIVED = false;\n\n    private OwoHandshake() {}\n\n    // ------------\n    // Registration\n    // ------------\n\n    public static void enable() {\n        if (OwoFreezer.isFrozen()) {\n            throw new ServicesFrozenException(\"The oωo handshake may only be enabled during mod initialization\");\n        }\n    }\n\n    public static void requireHandshake() {\n        if (OwoFreezer.isFrozen()) {\n            throw new ServicesFrozenException(\"The oωo handshake may only be made required during mod initialization\");\n        }\n\n        HANDSHAKE_REQUIRED = true;\n    }\n\n    static {\n        PayloadTypeRegistry.configurationS2C().register(HandshakeRequest.ID, CodecUtils.toPacketCodec(HandshakeRequest.ENDEC));\n        PayloadTypeRegistry.configurationC2S().register(HandshakeResponse.ID, CodecUtils.toPacketCodec(HandshakeResponse.ENDEC));\n\n        ServerConfigurationConnectionEvents.CONFIGURE.register(OwoHandshake::configureStart);\n        ServerConfigurationNetworking.registerGlobalReceiver(HandshakeResponse.ID, OwoHandshake::syncServer);\n\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {\n            if (!ENABLED) {\n                PayloadTypeRegistry.configurationS2C().register(HandshakeOff.ID, StreamCodec.unit(new HandshakeOff()));\n                ClientConfigurationNetworking.registerGlobalReceiver(HandshakeOff.ID, (payload, context) -> {});\n            }\n\n            ClientConfigurationNetworking.registerGlobalReceiver(HandshakeRequest.ID, OwoHandshake::syncClient);\n            ClientConfigurationConnectionEvents.READY.register(OwoHandshake::handleReadyClient);\n\n            ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {\n                QUERY_RECEIVED = false;\n                QueuedChannelSet.channels = null;\n            });\n\n            ClientConfigurationConnectionEvents.DISCONNECT.register((handler, client) -> {\n                QUERY_RECEIVED = false;\n                QueuedChannelSet.channels = null;\n            });\n        }\n    }\n\n    public static boolean isValidClient() {\n        return ENABLED && QUERY_RECEIVED;\n    }\n\n    // -------\n    // Packets\n    // -------\n\n    private static void configureStart(ServerConfigurationPacketListenerImpl handler, MinecraftServer server) {\n        if (!ENABLED) return;\n\n        if (ServerConfigurationNetworking.canSend(handler, OFF_CHANNEL_ID)) {\n            Owo.LOGGER.info(\"[Handshake] Handshake disabled by client, skipping\");\n            return;\n        }\n\n        if (!ServerConfigurationNetworking.canSend(handler, CHANNEL_ID)) {\n            if (!HANDSHAKE_REQUIRED) return;\n\n            handler.disconnect(TextOps.concat(PREFIX, Component.nullToEmpty(\"incompatible client\")));\n            Owo.LOGGER.info(\"[Handshake] Handshake failed, client doesn't understand channel packet\");\n            return;\n        }\n\n        var optionalChannels = formatHashes(OwoNetChannel.OPTIONAL_CHANNELS, OwoHandshake::hashChannel);\n        ServerConfigurationNetworking.send(handler, new HandshakeRequest(optionalChannels));\n        Owo.LOGGER.info(\"[Handshake] Sending channel packet\");\n    }\n\n    @Environment(EnvType.CLIENT)\n    private static void syncClient(HandshakeRequest request, ClientConfigurationNetworking.Context context) {\n        Owo.LOGGER.info(\"[Handshake] Sending client channels\");\n        QUERY_RECEIVED = true;\n\n        QueuedChannelSet.channels = filterOptionalServices(request.optionalChannels(), OwoNetChannel.REGISTERED_CHANNELS, OwoHandshake::hashChannel);\n\n        var requiredChannels = formatHashes(OwoNetChannel.REQUIRED_CHANNELS, OwoHandshake::hashChannel);\n        var requiredControllers = formatHashes(ParticleSystemController.REGISTERED_CONTROLLERS, OwoHandshake::hashController);\n        var optionalChannels = formatHashes(OwoNetChannel.OPTIONAL_CHANNELS, OwoHandshake::hashChannel);\n\n        context.responseSender().sendPacket(new HandshakeResponse(requiredChannels, requiredControllers, optionalChannels));\n    }\n\n    private static void syncServer(HandshakeResponse response, ServerConfigurationNetworking.Context context) {\n        Owo.LOGGER.info(\"[Handshake] Receiving client channels\");\n\n        StringBuilder disconnectMessage = new StringBuilder();\n\n        boolean isAllGood = verifyReceivedHashes(\"channels\", response.requiredChannels(), OwoNetChannel.REQUIRED_CHANNELS, OwoHandshake::hashChannel, disconnectMessage);\n        isAllGood &= verifyReceivedHashes(\"controllers\", response.requiredControllers(), ParticleSystemController.REGISTERED_CONTROLLERS, OwoHandshake::hashController, disconnectMessage);\n\n        if (!isAllGood) {\n            context.responseSender().disconnect(TextOps.concat(PREFIX, Component.nullToEmpty(disconnectMessage.toString())));\n        }\n\n        ((OwoClientConnectionExtension) ((ServerCommonPacketListenerImplAccessor) context.networkHandler()).owo$getConnection()).owo$setChannelSet(filterOptionalServices(response.optionalChannels(), OwoNetChannel.OPTIONAL_CHANNELS, OwoHandshake::hashChannel));\n\n        Owo.LOGGER.info(\"[Handshake] Handshake completed successfully\");\n    }\n\n    @Environment(EnvType.CLIENT)\n    private static void handleReadyClient(ClientConfigurationPacketListenerImpl handler, Minecraft client) {\n        if (ClientConfigurationNetworking.canSend(CHANNEL_ID) || !HANDSHAKE_REQUIRED || !ENABLED) return;\n\n        client.execute(() -> {\n            ((ClientCommonPacketListenerImplAccessor) handler)\n                    .getConnection()\n                    .disconnect(TextOps.concat(PREFIX, Component.nullToEmpty(\"incompatible server\")));\n        });\n    }\n\n    // -------\n    // Utility\n    // -------\n\n    private static <T> Set<Identifier> filterOptionalServices(Map<Identifier, Integer> remoteMap, Map<Identifier, T> localMap, ToIntFunction<T> hashFunction) {\n        Set<Identifier> readableServices = new HashSet<>();\n\n        for (var entry : remoteMap.entrySet()) {\n            var service = localMap.get(entry.getKey());\n\n            if (service == null) continue;\n            if (hashFunction.applyAsInt(service) != entry.getValue()) continue;\n\n            readableServices.add(entry.getKey());\n        }\n\n        return readableServices;\n    }\n\n    private static <T> boolean verifyReceivedHashes(String serviceNamePlural, Map<Identifier, Integer> clientMap, Map<Identifier, T> serverMap, ToIntFunction<T> hashFunction, StringBuilder disconnectMessage) {\n        boolean isAllGood = true;\n\n        if (!clientMap.keySet().equals(serverMap.keySet())) {\n            isAllGood = false;\n\n            var leftovers = findCollisions(clientMap.keySet(), serverMap.keySet());\n\n            if (!leftovers.getA().isEmpty()) {\n                disconnectMessage.append(\"server is missing \").append(serviceNamePlural).append(\":\\n\");\n                leftovers.getA().forEach(identifier -> disconnectMessage.append(\"§7\").append(identifier).append(\"§r\\n\"));\n            }\n\n            if (!leftovers.getB().isEmpty()) {\n                disconnectMessage.append(\"client is missing \").append(serviceNamePlural).append(\":\\n\");\n                leftovers.getB().forEach(identifier -> disconnectMessage.append(\"§7\").append(identifier).append(\"§r\\n\"));\n            }\n        }\n\n        boolean hasMismatchedHashes = false;\n        for (var entry : clientMap.entrySet()) {\n            var actualServiceObject = serverMap.get(entry.getKey());\n            if (actualServiceObject == null) continue;\n\n            int localHash = hashFunction.applyAsInt(actualServiceObject);\n\n            if (localHash != entry.getValue()) {\n                if (!hasMismatchedHashes) {\n                    disconnectMessage.append(serviceNamePlural).append(\" with mismatched hashes:\\n\");\n                }\n\n                disconnectMessage.append(\"§7\").append(entry.getKey()).append(\"§r\\n\");\n\n                isAllGood = false;\n                hasMismatchedHashes = true;\n            }\n        }\n\n        return isAllGood;\n    }\n\n    private static <T> Map<Identifier, Integer> formatHashes(Map<Identifier, T> values, ToIntFunction<T> hashFunction) {\n        Map<Identifier, Integer> hashes = new HashMap<>();\n\n        for (var entry : values.entrySet()) {\n            hashes.put(entry.getKey(), hashFunction.applyAsInt(entry.getValue()));\n        }\n\n        return hashes;\n    }\n\n    private static Tuple<Set<Identifier>, Set<Identifier>> findCollisions(Set<Identifier> first, Set<Identifier> second) {\n        var firstLeftovers = new HashSet<Identifier>();\n        var secondLeftovers = new HashSet<Identifier>();\n\n        first.forEach(identifier -> {\n            if (!second.contains(identifier)) firstLeftovers.add(identifier);\n        });\n\n        second.forEach(identifier -> {\n            if (!first.contains(identifier)) secondLeftovers.add(identifier);\n        });\n\n        return new Tuple<>(firstLeftovers, secondLeftovers);\n    }\n\n    private static int hashChannel(OwoNetChannel channel) {\n        int serializersHash = 0;\n        for (var entry : channel.endecsByIndex.int2ObjectEntrySet()) {\n            serializersHash += entry.getIntKey() * 31 + entry.getValue().getRecordClass().getName().hashCode();\n        }\n        return 31 * channel.packetId.id().hashCode() + serializersHash;\n    }\n\n    private static int hashController(ParticleSystemController controller) {\n        int serializersHash = 0;\n        for (var entry : controller.systemsByIndex.int2ObjectEntrySet()) {\n            serializersHash += entry.getIntKey();\n        }\n        return 31 * controller.channelId.hashCode() + serializersHash;\n    }\n\n    public record HandshakeRequest(Map<Identifier, Integer> optionalChannels) implements CustomPacketPayload {\n\n        public static final Type<HandshakeRequest> ID = new Type<>(OwoHandshake.CHANNEL_ID);\n        public static final Endec<HandshakeRequest> ENDEC = StructEndecBuilder.of(\n                CHANNEL_HASHES_ENDEC.fieldOf(\"optionalChannels\", HandshakeRequest::optionalChannels),\n                HandshakeRequest::new\n        );\n\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return ID;\n        }\n    }\n\n    public record HandshakeOff() implements CustomPacketPayload {\n        public static final Type<HandshakeOff> ID = new Type<>(OwoHandshake.OFF_CHANNEL_ID);\n\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return ID;\n        }\n\n    }\n\n    private record HandshakeResponse(Map<Identifier, Integer> requiredChannels,\n                                     Map<Identifier, Integer> requiredControllers,\n                                     Map<Identifier, Integer> optionalChannels) implements CustomPacketPayload {\n\n        public static final Type<HandshakeResponse> ID = new Type<>(OwoHandshake.CHANNEL_ID);\n        public static final Endec<HandshakeResponse> ENDEC = StructEndecBuilder.of(\n                CHANNEL_HASHES_ENDEC.fieldOf(\"requiredChannels\", HandshakeResponse::requiredChannels),\n                CHANNEL_HASHES_ENDEC.fieldOf(\"requiredControllers\", HandshakeResponse::requiredControllers),\n                CHANNEL_HASHES_ENDEC.fieldOf(\"optionalChannels\", HandshakeResponse::optionalChannels),\n                HandshakeResponse::new\n        );\n\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return ID;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/network/OwoNetChannel.java",
    "content": "package io.wispforest.owo.network;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.StructEndec;\nimport io.wispforest.endec.impl.RecordEndec;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport io.wispforest.owo.mixin.ServerCommonPacketListenerImplAccessor;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.util.OwoFreezer;\nimport io.wispforest.owo.util.ReflectionUtils;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;\nimport it.unimi.dsi.fastutil.objects.Reference2IntMap;\nimport it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;\nimport net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;\nimport net.fabricmc.fabric.api.networking.v1.PlayerLookup;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.network.Connection;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.MinecraftServer;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.server.network.ServerGamePacketListenerImpl;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.level.block.entity.BlockEntity;\n\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\n/**\n * An efficient networking abstraction that uses {@code record}s to store\n * and define packet data. Serialization for most types is fully automatic\n * and no custom handling needs to be done.\n *\n * <p> Should one of your record components be of an unsupported type, either use {@link io.wispforest.endec.impl.ReflectiveEndecBuilder#register(Endec, Class)}\n * to register an appropriate endec, or supply it directly using {@link #registerClientbound(Class, StructEndec, ChannelHandler)} and {@link #registerServerbound(Class, StructEndec, ChannelHandler)}\n *\n * <p> To define a packet class suited for use with this wrapper, simply create a\n * standard Java {@code record} class and put the desired data into the record header.\n *\n * <p>To register a packet onto this channel, use either {@link #registerClientbound(Class, ChannelHandler)}\n * or {@link #registerServerbound(Class, ChannelHandler)}, depending on which direction the packet goes.\n * Bidirectional registration of the same class is explicitly supported. <b>For synchronization purposes,\n * all registration must happen on both client and server, even for clientbound packets. Otherwise,\n * joining the server will fail with a handshake error</b>\n *\n * <p>To send a packet, use any of the {@code handle} methods to obtain a handle for sending. These are\n * named after where the packet is sent <i>from</i>, meaning the {@link #clientHandle()} is used for sending\n * <i>to the server</i> and vice-versa.\n *\n * <p> The registered packet handlers are executed synchronously on the target environment's\n * game thread instead of Netty's event loops - there is no need to call {@code .execute(...)}\n */\npublic class OwoNetChannel {\n\n    static final Map<Identifier, OwoNetChannel> REGISTERED_CHANNELS = new HashMap<>();\n    static final Map<Identifier, OwoNetChannel> REQUIRED_CHANNELS = new HashMap<>();\n    static final Map<Identifier, OwoNetChannel> OPTIONAL_CHANNELS = new HashMap<>();\n\n    private final ReflectiveEndecBuilder builder;\n\n    private final Map<Class<?>, IndexedEndec<?>> endecsByClass = new HashMap<>();\n    final Int2ObjectMap<IndexedEndec<?>> endecsByIndex = new Int2ObjectOpenHashMap<>();\n\n    private final List<ChannelHandler<Record, ClientAccess>> clientHandlers = new ArrayList<>();\n    private final List<ChannelHandler<Record, ServerAccess>> serverHandlers = new ArrayList<>();\n\n    private final Reference2IntMap<Class<?>> deferredClientEndecs = new Reference2IntOpenHashMap<>();\n\n    final CustomPacketPayload.Type<MessagePayload> packetId;\n    private final String ownerClassName;\n    final boolean required;\n\n    private ClientHandle clientHandle = null;\n    private ServerHandle serverHandle = null;\n\n    /**\n     * Creates a new required channel with given ID. Duplicate channel\n     * IDs are not allowed - if there is a collision, the name of the\n     * class that previously registered the channel will be part of\n     * the exception. <b>This may be called at any stage during\n     * mod initialization</b>\n     *\n     * @param id The desired channel ID\n     * @return The created channel\n     */\n    public static OwoNetChannel create(Identifier id) {\n        return new OwoNetChannel(id, ReflectionUtils.getCallingClassName(2), true);\n    }\n\n    /**\n     * Creates a new optional channel with given ID. Duplicate channel\n     * IDs are not allowed - if there is a collision, the name of the\n     * class that previously registered the channel will be part of\n     * the exception. <b>This may be called at any stage during\n     * mod initialization</b>\n     *\n     * @param id The desired channel ID\n     * @return The created channel\n     */\n    public static OwoNetChannel createOptional(Identifier id) {\n        return new OwoNetChannel(id, ReflectionUtils.getCallingClassName(2), false);\n    }\n\n    private OwoNetChannel(Identifier id, String ownerClassName, boolean required) {\n        OwoFreezer.checkRegister(\"Network channels\");\n\n        this.builder = new ReflectiveEndecBuilder(builder -> {\n            builder.register(Endec.VAR_INT, Integer.class, int.class);\n            builder.register(Endec.VAR_LONG, Long.class, long.class);\n            MinecraftEndecs.addDefaults(builder);\n        });\n\n        if (REGISTERED_CHANNELS.containsKey(id)) {\n            throw new IllegalStateException(\"Channel with id '\" + id + \"' was already registered from class '\" + REGISTERED_CHANNELS.get(id).ownerClassName + \"'\");\n        }\n\n        this.deferredClientEndecs.defaultReturnValue(-1);\n\n        this.packetId = new CustomPacketPayload.Type<>(id);\n        this.ownerClassName = ownerClassName;\n        this.required = required;\n\n        OwoHandshake.enable();\n        if (required) {\n            OwoHandshake.requireHandshake();\n        }\n\n        Endec<MessagePayload> serverEndec = Endec.<Record, Integer>dispatched(\n            index -> this.endecsByIndex.get(index).endec,\n            msg -> this.endecsByClass.get(msg.getClass()).serverHandlerIndex,\n            Endec.VAR_INT\n        )\n            .xmap(x -> new MessagePayload(this.packetId, x), x -> x.message);\n\n        Endec<MessagePayload> clientEndec = Endec.<Record, Integer>dispatched(\n                index -> this.endecsByIndex.get(-index).endec,\n                msg -> this.endecsByClass.get(msg.getClass()).clientHandlerIndex,\n                Endec.VAR_INT\n            )\n            .xmap(x -> new MessagePayload(this.packetId, x), x -> x.message);\n\n        PayloadTypeRegistry.playC2S().register(this.packetId, CodecUtils.toPacketCodec(serverEndec));\n        PayloadTypeRegistry.playS2C().register(this.packetId, CodecUtils.toPacketCodec(clientEndec));\n\n        ServerPlayNetworking.registerGlobalReceiver(this.packetId, (payload, context) -> {\n            serverHandlers.get(endecsByClass.get(payload.message().getClass()).serverHandlerIndex).handle(payload.message, new ServerAccess(context.player()));\n        });\n\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {\n            ClientPlayNetworking.registerGlobalReceiver(this.packetId, (payload, context) -> {\n                clientHandlers.get(endecsByClass.get(payload.message.getClass()).clientHandlerIndex).handle(payload.message, new ClientAccess(context.player().connection));\n            });\n        }\n\n        clientHandlers.add(null);\n        serverHandlers.add(null);\n        REGISTERED_CHANNELS.put(id, this);\n\n        if (required) {\n            REQUIRED_CHANNELS.put(id, this);\n        } else {\n            OPTIONAL_CHANNELS.put(id, this);\n        }\n    }\n\n    public OwoNetChannel addEndecs(Consumer<ReflectiveEndecBuilder> endecBuilder) {\n        endecBuilder.accept(this.builder);\n\n        return this;\n    }\n\n    public ReflectiveEndecBuilder builder() {\n        return this.builder;\n    }\n\n    /**\n     * Registers a handler <i>on the client</i> for the specified message class.\n     * This also ensures the required endec is available. If an exception\n     * about a missing endec is thrown, register one\n     *\n     * @param messageClass The type of packet data to send and serialize\n     * @param handler      The handler that will receive the deserialized\n     * @see #serverHandle(Player)\n     * @see #serverHandle(MinecraftServer)\n     * @see #serverHandle(ServerLevel, BlockPos)\n     */\n    public <R extends Record> void registerClientbound(Class<R> messageClass, ChannelHandler<R, ClientAccess> handler) {\n        registerClientbound(messageClass, handler, () -> RecordEndec.create(this.builder, messageClass));\n    }\n\n    /**\n     * Registers a message class <i>on the client</i> with deferred handler registration.\n     * This also ensures the required endec is available. If an exception\n     * about a missing endec is thrown, register one\n     *\n     * @param messageClass The type of packet data to send and serialize\n     * @see #serverHandle(Player)\n     * @see #serverHandle(MinecraftServer)\n     * @see #serverHandle(ServerLevel, BlockPos)\n     */\n    public <R extends Record> void registerClientboundDeferred(Class<R> messageClass) {\n        registerClientboundDeferred(messageClass, () -> RecordEndec.create(this.builder, messageClass));\n    }\n\n    /**\n     * Registers a handler <i>on the server</i> for the specified message class.\n     * This also ensures the required endec is available. If an exception\n     * about a missing endec is thrown, register one\n     *\n     * @param messageClass The type of packet data to send and serialize\n     * @param handler      The handler that will receive the deserialized\n     * @see #clientHandle()\n     */\n    public <R extends Record> void registerServerbound(Class<R> messageClass, ChannelHandler<R, ServerAccess> handler) {\n        registerServerbound(messageClass, handler, () -> RecordEndec.create(this.builder, messageClass));\n    }\n\n    //--\n\n    /**\n     * Registers a handler <i>on the client</i> for the specified message class\n     *\n     * @param messageClass The type of packet data to send and serialize\n     * @param endec        The endec to serialize messages with\n     * @param handler      The handler that will receive the deserialized\n     * @see #serverHandle(Player)\n     * @see #serverHandle(MinecraftServer)\n     * @see #serverHandle(ServerLevel, BlockPos)\n     */\n    public <R extends Record> void registerClientbound(Class<R> messageClass, StructEndec<R> endec, ChannelHandler<R, ClientAccess> handler) {\n        registerClientbound(messageClass, handler, () -> endec);\n    }\n\n    /**\n     * Registers a message class <i>on the client</i> with deferred handler registration\n     *\n     * @param messageClass The type of packet data to send and serialize\n     * @param endec        The endec to serialize messages with\n     * @see #serverHandle(Player)\n     * @see #serverHandle(MinecraftServer)\n     * @see #serverHandle(ServerLevel, BlockPos)\n     */\n    public <R extends Record> void registerClientboundDeferred(Class<R> messageClass, StructEndec<R> endec) {\n        registerClientboundDeferred(messageClass, () -> endec);\n    }\n\n    /**\n     * Registers a handler <i>on the server</i> for the specified message class\n     *\n     * @param messageClass The type of packet data to send and serialize\n     * @param endec        The endec to serialize messages with\n     * @param handler      The handler that will receive the deserialized\n     * @see #clientHandle()\n     */\n    public <R extends Record> void registerServerbound(Class<R> messageClass, StructEndec<R> endec, ChannelHandler<R, ServerAccess> handler) {\n        registerServerbound(messageClass, handler, () -> endec);\n    }\n\n    //--\n\n    @SuppressWarnings(\"unchecked\")\n    private  <R extends Record> void registerClientbound(Class<R> messageClass, ChannelHandler<R, ClientAccess> handler, Supplier<StructEndec<R>> endec) {\n        int deferredIndex = deferredClientEndecs.removeInt(messageClass);\n        if (deferredIndex != -1) {\n            OwoFreezer.checkRegister(\"Network handlers\");\n\n            this.clientHandlers.set(deferredIndex, (ChannelHandler<Record, ClientAccess>) handler);\n            return;\n        }\n\n        int index = this.clientHandlers.size();\n        this.createEndec(messageClass, index, EnvType.CLIENT, endec);\n        this.clientHandlers.add((ChannelHandler<Record, ClientAccess>) handler);\n    }\n\n    private <R extends Record> void registerClientboundDeferred(Class<R> messageClass, Supplier<StructEndec<R>> endec) {\n        int index = this.clientHandlers.size();\n        this.createEndec(messageClass, index, EnvType.CLIENT, endec);\n        this.clientHandlers.add(null);\n\n        this.deferredClientEndecs.put(messageClass, index);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <R extends Record> void registerServerbound(Class<R> messageClass, ChannelHandler<R, ServerAccess> handler, Supplier<StructEndec<R>> endec) {\n        int index = this.serverHandlers.size();\n        this.createEndec(messageClass, index, EnvType.SERVER, endec);\n        this.serverHandlers.add((ChannelHandler<Record, ServerAccess>) handler);\n    }\n\n    //--\n\n    public boolean canSendToPlayer(ServerPlayer player) {\n        return canSendToPlayer(player.connection);\n    }\n\n    public boolean canSendToPlayer(ServerGamePacketListenerImpl networkHandler) {\n        if (required) return true;\n\n        return OwoHandshake.isValidClient() ?\n                getChannelSet(((ServerCommonPacketListenerImplAccessor) networkHandler).owo$getConnection()).contains(this.packetId.id())\n                : ServerPlayNetworking.canSend(networkHandler, this.packetId);\n    }\n\n    @Environment(EnvType.CLIENT)\n    public boolean canSendToServer() {\n        if (required) return true;\n\n        return OwoHandshake.isValidClient() ?\n                getChannelSet(Minecraft.getInstance().getConnection().getConnection()).contains(this.packetId.id())\n                : ClientPlayNetworking.canSend(this.packetId);\n    }\n\n    private static Set<Identifier> getChannelSet(Connection connection) {\n        return ((OwoClientConnectionExtension) connection).owo$getChannelSet();\n    }\n\n    /**\n     * Obtains the client handle of this channel, used to\n     * send packets <i>to the server</i>\n     *\n     * @return The client handle of this channel\n     */\n    public ClientHandle clientHandle() {\n        if (FabricLoader.getInstance().getEnvironmentType() != EnvType.CLIENT)\n            throw new NetworkException(\"Cannot obtain client handle in environment type '\" + FabricLoader.getInstance().getEnvironmentType() + \"'\");\n\n        if (this.clientHandle == null) this.clientHandle = new ClientHandle();\n        return clientHandle;\n    }\n\n    /**\n     * Obtains a server handle used to send packets\n     * <i>to all players on the given server</i>\n     * <p>\n     * <b>This handle will be reused - do not retain references</b>\n     *\n     * @param server The server to target\n     * @return A server handle configured for sending packets\n     * to all players on the given server\n     */\n    public ServerHandle serverHandle(MinecraftServer server) {\n        var handle = getServerHandle();\n        handle.targets = PlayerLookup.all(server);\n        return handle;\n    }\n\n    /**\n     * Obtains a server handle used to send packets\n     * <i>to all given players</i>. Use {@link PlayerLookup} to obtain\n     * the required collections\n     * <p>\n     * <b>This handle will be reused - do not retain references</b>\n     *\n     * @param targets The players to target\n     * @return A server handle configured for sending packets\n     * to all players in the given collection\n     * @see PlayerLookup\n     */\n    public ServerHandle serverHandle(Collection<ServerPlayer> targets) {\n        var handle = getServerHandle();\n        handle.targets = targets;\n        return handle;\n    }\n\n    /**\n     * Obtains a server handle used to send packets\n     * <i>to the given player only</i>\n     * <p>\n     * <b>This handle will be reused - do not retain references</b>\n     *\n     * @param player The player to target\n     * @return A server handle configured for sending packets\n     * to the given player only\n     */\n    public ServerHandle serverHandle(Player player) {\n        if (!(player instanceof ServerPlayer serverPlayer)) throw new NetworkException(\"'player' must be a 'ServerPlayerEntity'\");\n\n        var handle = getServerHandle();\n        handle.targets = Collections.singleton(serverPlayer);\n        return handle;\n    }\n\n    /**\n     * Obtains a server handle used to send packets\n     * <i>to all players tracking the given block entity</i>\n     * <p>\n     * <b>This handle will be reused - do not retain references</b>\n     *\n     * @param entity The block entity to look up trackers for\n     * @return A server handle configured for sending packets\n     * to all players tracking the given block entity\n     */\n    public ServerHandle serverHandle(BlockEntity entity) {\n        if (entity.getLevel().isClientSide()) throw new NetworkException(\"Server handle cannot be obtained on the client\");\n        return serverHandle(PlayerLookup.tracking(entity));\n    }\n\n    /**\n     * Obtains a server handle used to send packets <i>to all\n     * players tracking the given position in the given world</i>\n     * <p>\n     * <b>This handle will be reused - do not retain references</b>\n     *\n     * @param world The world to look up players in\n     * @param pos   The position to look up trackers for\n     * @return A server handle configured for sending packets\n     * to all players tracking the given position in the given world\n     */\n    public ServerHandle serverHandle(ServerLevel world, BlockPos pos) {\n        return serverHandle(PlayerLookup.tracking(world, pos));\n    }\n\n    private ServerHandle getServerHandle() {\n        if (this.serverHandle == null) this.serverHandle = new ServerHandle();\n        return serverHandle;\n    }\n\n    private <R extends Record> void createEndec(Class<R> messageClass, int handlerIndex, EnvType target, Supplier<StructEndec<R>> supplier) {\n        OwoFreezer.checkRegister(\"Network handlers\");\n\n        var endec = endecsByClass.get(messageClass);\n        if (endec == null) {\n            final var indexedEndec = IndexedEndec.create(messageClass, supplier.get(), handlerIndex, target);\n            endecsByClass.put(messageClass, indexedEndec);\n            endecsByIndex.put(target == EnvType.CLIENT ? -handlerIndex : handlerIndex, indexedEndec);\n        } else if (endec.handlerIndex(target) == -1) {\n            endec.setHandlerIndex(handlerIndex, target);\n            endecsByIndex.put(target == EnvType.CLIENT ? -handlerIndex : handlerIndex, endec);\n        } else {\n            throw new IllegalStateException(\"Message class '\" + messageClass.getName() + \"' is already registered for target environment \" + target);\n        }\n    }\n\n    public class ClientHandle {\n\n        /**\n         * Sends the given message to the server\n         *\n         * @param message The message to send\n         * @see #send(Record[])\n         */\n        public <R extends Record> void send(R message) {\n            ClientPlayNetworking.send(new MessagePayload(packetId, message));\n        }\n\n        /**\n         * Sends the given messages to the server\n         *\n         * @param messages The messages to send\n         */\n        @SafeVarargs\n        public final <R extends Record> void send(R... messages) {\n            for (R message : messages) send(message);\n        }\n    }\n\n    public class ServerHandle {\n\n        private Collection<ServerPlayer> targets = Collections.emptySet();\n\n        /**\n         * Sends the given message to the configured target(s)\n         * <b>Resets the target(s) after sending - this cannot be used\n         * for multiple messages on the same handle</b>\n         *\n         * @param message The message to send\n         * @see #send(Record[])\n         */\n        public <R extends Record> void send(R message) {\n            this.targets.forEach(player -> ServerPlayNetworking.send(player, new MessagePayload(packetId, message)));\n            this.targets = null;\n        }\n\n        /**\n         * Sends the given messages to the configured target(s)\n         * <b>Resets the target(s) after sending - this cannot be used\n         * multiple times on the same handle</b>\n         *\n         * @param messages The messages to send\n         */\n        @SafeVarargs\n        public final <R extends Record> void send(R... messages) {\n            this.targets.forEach(player -> {\n                for (R message : messages) {\n                    ServerPlayNetworking.send(player, new MessagePayload(packetId, message));\n                }\n            });\n            this.targets = null;\n        }\n    }\n\n    public interface ChannelHandler<R extends Record, E extends EnvironmentAccess<?, ?, ?>> {\n\n        /**\n         * Executed on the game thread to handle the incoming\n         * message - this can safely modify game state\n         *\n         * @param message The message that was received\n         * @param access  The {@link EnvironmentAccess} used to obtain references\n         *                to the execution environment\n         */\n        void handle(R message, E access);\n    }\n\n    /**\n     * A simple wrapper that provides access to the environment a packet\n     * is being received / message is being handled in\n     *\n     * @param <P> The type of player to receive the packet\n     * @param <R> The runtime that the packet is being received in\n     * @param <N> The network handler that received the packet\n     */\n    public interface EnvironmentAccess<P extends Player, R, N> {\n\n        /**\n         * @return The player that received the packet\n         */\n        P player();\n\n        /**\n         * @return The environment the packet is being received in,\n         * either a {@link MinecraftServer} or a {@link net.minecraft.client.Minecraft}\n         */\n        R runtime();\n\n        /**\n         * @return The network handler of the player or client that received the packet,\n         * either a {@link net.minecraft.client.multiplayer.ClientPacketListener} or a\n         * {@link net.minecraft.server.network.ServerGamePacketListenerImpl}\n         */\n        N packetListener();\n    }\n\n    private void verify() {\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {\n            if (!this.deferredClientEndecs.isEmpty()) {\n                throw new NetworkException(\"Some deferred client handlers for channel \" + this.packetId + \" haven't been registered: \" + deferredClientEndecs.keySet().stream().map(Class::getName).collect(Collectors.joining(\", \")));\n            }\n        }\n    }\n\n    static {\n        OwoFreezer.registerFreezeCallback(() -> {\n            for (OwoNetChannel channel : OwoNetChannel.REGISTERED_CHANNELS.values()) {\n                channel.verify();\n            }\n        });\n    }\n\n    static final class IndexedEndec<R extends Record> {\n        private int clientHandlerIndex = -1;\n        private int serverHandlerIndex = -1;\n\n        private final Class<R> recordClass;\n        private final StructEndec<R> endec;\n\n        private IndexedEndec(Class<R> recordClass, StructEndec<R> endec) {\n            this.endec = endec;\n            this.recordClass = recordClass;\n        }\n\n        public static <R extends Record> IndexedEndec<R> create(Class<R> rClass, StructEndec<R> endec, int index, EnvType target) {\n            return new IndexedEndec<>(rClass, endec).setHandlerIndex(index, target);\n        }\n\n        public IndexedEndec<R> setHandlerIndex(int index, EnvType target) {\n            switch (target) {\n                case CLIENT -> this.clientHandlerIndex = index;\n                case SERVER -> this.serverHandlerIndex = index;\n            }\n            return this;\n        }\n\n        public int handlerIndex(EnvType target) {\n            return switch (target) {\n                case CLIENT -> clientHandlerIndex;\n                case SERVER -> serverHandlerIndex;\n            };\n        }\n\n        public Class<R> getRecordClass(){\n            return this.recordClass;\n        }\n    }\n\n    record MessagePayload(CustomPacketPayload.Type<MessagePayload> id, Record message) implements CustomPacketPayload {\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return id;\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/network/QueuedChannelSet.java",
    "content": "package io.wispforest.owo.network;\n\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.Set;\n\n@ApiStatus.Internal\n@Environment(EnvType.CLIENT)\npublic class QueuedChannelSet {\n    public static Set<Identifier> channels;\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/network/ServerAccess.java",
    "content": "package io.wispforest.owo.network;\n\nimport net.minecraft.server.MinecraftServer;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.server.network.ServerGamePacketListenerImpl;\n\npublic record ServerAccess(ServerPlayer player) implements\n        OwoNetChannel.EnvironmentAccess<ServerPlayer, MinecraftServer, ServerGamePacketListenerImpl> {\n\n    @Override\n    public MinecraftServer runtime() {\n        return player.level().getServer();\n    }\n\n    @Override\n    public ServerGamePacketListenerImpl packetListener() {\n        return player.connection;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ops/ItemOps.java",
    "content": "package io.wispforest.owo.ops;\n\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.ItemStack;\n\n/**\n * A collection of common checks and operations done on {@link ItemStack}\n */\npublic final class ItemOps {\n\n    private ItemOps() {\n    }\n\n    /**\n     * Checks if stack one can stack onto stack two\n     *\n     * @param base     The base stack\n     * @param addition The stack to be added\n     * @return {@code true} if addition can stack onto base\n     */\n    public static boolean canStack(ItemStack base, ItemStack addition) {\n        return base.isEmpty() || (canIncreaseBy(base, addition.getCount()) && ItemStack.isSameItemSameComponents(base, addition));\n    }\n\n    /**\n     * Checks if a stack can increase\n     *\n     * @param stack The stack to test\n     * @return stack.getCount() &lt; stack.getMaxCount()\n     */\n    public static boolean canIncrease(ItemStack stack) {\n        return stack.isStackable() && stack.getCount() < stack.getMaxStackSize();\n    }\n\n    /**\n     * Checks if a stack can increase by the given amount\n     *\n     * @param stack The stack to test\n     * @param by    The amount to test for\n     * @return {@code true} if the stack can increase by the given amount\n     */\n    public static boolean canIncreaseBy(ItemStack stack, int by) {\n        return stack.isStackable() && stack.getCount() + by <= stack.getMaxStackSize();\n    }\n\n    /**\n     * Returns a copy of the given stack with count set to 1\n     */\n    public static ItemStack singleCopy(ItemStack stack) {\n        ItemStack copy = stack.copy();\n        copy.setCount(1);\n        return copy;\n    }\n\n    /**\n     * Decrements the stack\n     *\n     * @param stack The stack to decrement\n     * @return {@code false} if the stack is empty after the operation\n     */\n    public static boolean emptyAwareDecrement(ItemStack stack) {\n        return emptyAwareDecrement(stack, 1);\n    }\n\n    /**\n     * Decrements the stack\n     *\n     * @param stack  The stack to decrement\n     * @param amount The amount to decrement\n     * @return {@code false} if the stack is empty after the operation\n     */\n    public static boolean emptyAwareDecrement(ItemStack stack, int amount) {\n        stack.shrink(amount);\n        return !stack.isEmpty();\n    }\n\n    /**\n     * Decrements the stack in the players hand and replaces it with {@link ItemStack#EMPTY}\n     * if the result would be an empty stack\n     *\n     * @param player The player to operate on\n     * @param hand   The hand to affect\n     * @return {@code false} if the stack is empty after the operation\n     */\n    public static boolean decrementPlayerHandItem(Player player, InteractionHand hand) {\n        return decrementPlayerHandItem(player, hand, 1);\n    }\n\n    /**\n     * Decrements the stack in the players hand and replaces it with {@link ItemStack#EMPTY}\n     * if the result would be an empty stack\n     *\n     * @param player The player to operate on\n     * @param hand   The hand to affect\n     * @param amount The amount to decrement\n     * @return {@code false} if the stack is empty after the operation\n     */\n    public static boolean decrementPlayerHandItem(Player player, InteractionHand hand, int amount) {\n        var stack = player.getItemInHand(hand);\n        if (!player.isCreative()) {\n            if (!emptyAwareDecrement(stack, amount)) player.setItemInHand(hand, ItemStack.EMPTY);\n        }\n        return !stack.isEmpty();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ops/LevelOps.java",
    "content": "package io.wispforest.owo.ops;\n\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.sounds.SoundEvent;\nimport net.minecraft.sounds.SoundSource;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.EntityBlock;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Set;\n\n/**\n * A collection of common operations done on {@link Level}\n */\npublic final class LevelOps {\n\n    private LevelOps() {}\n\n    /**\n     * Break the specified block with the given item\n     *\n     * @param level     The level the block is in\n     * @param pos       The position of the block to break\n     * @param breakItem The item to break the block with\n     */\n    public static void breakBlockWithItem(Level level, BlockPos pos, ItemStack breakItem) {\n        breakBlockWithItem(level, pos, breakItem, null);\n    }\n\n    /**\n     * Break the specified block with the given item\n     *\n     * @param level          The level the block is in\n     * @param pos            The position of the block to break\n     * @param breakItem      The item to break the block with\n     * @param breakingEntity The entity which is breaking the block\n     */\n    public static void breakBlockWithItem(Level level, BlockPos pos, ItemStack breakItem, @Nullable Entity breakingEntity) {\n        BlockEntity breakEntity = level.getBlockState(pos).getBlock() instanceof EntityBlock ? level.getBlockEntity(pos) : null;\n        Block.dropResources(level.getBlockState(pos), level, pos, breakEntity, breakingEntity, breakItem);\n        level.destroyBlock(pos, false, breakingEntity);\n    }\n\n    /**\n     * Plays the provided sound at the provided location. This works on both client\n     * and server. Volume and pitch default to 1\n     *\n     * @param level    The level to play the sound in\n     * @param pos      Where to play the sound\n     * @param sound    The sound to play\n     * @param category The category for the sound\n     */\n    public static void playSound(Level level, Vec3 pos, SoundEvent sound, SoundSource category) {\n        playSound(level, BlockPos.containing(pos), sound, category, 1, 1);\n    }\n\n    public static void playSound(Level level, BlockPos pos, SoundEvent sound, SoundSource category) {\n        playSound(level, pos, sound, category, 1, 1);\n    }\n\n    /**\n     * Plays the provided sound at the provided location. This works on both client\n     * and server\n     *\n     * @param level    The level to play the sound in\n     * @param pos      Where to play the sound\n     * @param sound    The sound to play\n     * @param category The category for the sound\n     * @param volume   The volume to play the sound at\n     * @param pitch    The pitch, or speed, to play the sound at\n     */\n    public static void playSound(Level level, Vec3 pos, SoundEvent sound, SoundSource category, float volume, float pitch) {\n        level.playSound(null, BlockPos.containing(pos), sound, category, volume, pitch);\n    }\n\n    public static void playSound(Level level, BlockPos pos, SoundEvent sound, SoundSource category, float volume, float pitch) {\n        level.playSound(null, pos, sound, category, volume, pitch);\n    }\n\n    /**\n     * Causes a block update at the given position, if {@code level}\n     * is an instance of {@link ServerLevel}\n     *\n     * @param level The target level\n     * @param pos   The target position\n     */\n    public static void updateIfOnServer(Level level, BlockPos pos) {\n        if (!(level instanceof ServerLevel serverWorld)) return;\n        serverWorld.getChunkSource().blockChanged(pos);\n    }\n\n    /**\n     * Same as {@link LevelOps#teleportToLevel(ServerPlayer, ServerLevel, Vec3, float, float)} but defaults\n     * to {@code 0} for {@code pitch} and {@code yaw}\n     */\n    public static void teleportToLevel(ServerPlayer player, ServerLevel target, Vec3 pos) {\n        teleportToLevel(player, target, pos, 0, 0);\n    }\n\n    /**\n     * Teleports the given player to the given world, syncing all the annoying data\n     * like experience and status effects that minecraft doesn't\n     *\n     * @param player The player to teleport\n     * @param target The level to teleport to\n     * @param pos    The target position\n     * @param yaw    The target yaw\n     * @param pitch  The target pitch\n     */\n    public static void teleportToLevel(ServerPlayer player, ServerLevel target, Vec3 pos, float yaw, float pitch) {\n        player.teleportTo(target, pos.x, pos.y, pos.z, Set.of(), yaw, pitch, false);\n        player.giveExperiencePoints(0);\n\n        player.getActiveEffects().forEach(effect -> {\n            player.connection.send(new ClientboundUpdateMobEffectPacket(player.getId(), effect, false));\n        });\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ops/LootOps.java",
    "content": "package io.wispforest.owo.ops;\n\nimport io.wispforest.owo.mixin.SetComponentsFunctionAccessor;\nimport net.fabricmc.fabric.api.loot.v3.LootTableEvents;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.ItemLike;\nimport net.minecraft.world.level.storage.loot.LootPool;\nimport net.minecraft.world.level.storage.loot.entries.LootItem;\nimport net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer;\nimport net.minecraft.world.level.storage.loot.functions.SetItemCountFunction;\nimport net.minecraft.world.level.storage.loot.predicates.LootItemRandomChanceCondition;\nimport net.minecraft.world.level.storage.loot.providers.number.ConstantValue;\nimport net.minecraft.world.level.storage.loot.providers.number.UniformGenerator;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\n/**\n * A simple utility class to make injecting simple items or\n * ItemStacks into one or multiple LootTables a one-line operation\n */\npublic final class LootOps {\n\n    private LootOps() {}\n\n    private static final Map<Identifier[], Supplier<LootPoolEntryContainer>> ADDITIONS = new HashMap<>();\n\n    /**\n     * Injects a single item entry into the specified LootTable(s)\n     *\n     * @param item         The item to inject\n     * @param chance       The chance for the item to actually generate\n     * @param targetTables The LootTable(s) to inject into\n     */\n    public static void injectItem(ItemLike item, float chance, Identifier... targetTables) {\n        ADDITIONS.put(targetTables, () -> LootItem.lootTableItem(item).when(LootItemRandomChanceCondition.randomChance(chance)).build());\n    }\n\n    /**\n     * Injects an item entry into the specified LootTable(s),\n     * with a random count between {@code min} and {@code max}\n     *\n     * @param item         The item to inject\n     * @param chance       The chance for the item to actually generate\n     * @param min          The minimum amount of items to generate\n     * @param max          The maximum amount of items to generate\n     * @param targetTables The LootTable(s) to inject into\n     */\n    public static void injectItemWithCount(ItemLike item, float chance, int min, int max, Identifier... targetTables) {\n        ADDITIONS.put(targetTables, () -> LootItem.lootTableItem(item)\n                .when(LootItemRandomChanceCondition.randomChance(chance))\n                .apply(SetItemCountFunction.setCount(UniformGenerator.between(min, max)))\n                .build());\n    }\n\n    /**\n     * Injects a single ItemStack entry into the specified LootTable(s)\n     *\n     * @param stack        The ItemStack to inject\n     * @param chance       The chance for the ItemStack to actually generate\n     * @param targetTables The LootTable(s) to inject into\n     */\n    public static void injectItemStack(ItemStack stack, float chance, Identifier... targetTables) {\n        ADDITIONS.put(targetTables, () -> LootItem.lootTableItem(stack.getItem())\n                .when(LootItemRandomChanceCondition.randomChance(chance))\n                .apply(() -> SetComponentsFunctionAccessor.createSetComponentsLootFunction(List.of(), stack.getComponentsPatch()))\n                .apply(SetItemCountFunction.setCount(ConstantValue.exactly(stack.getCount())))\n                .build());\n    }\n\n    /**\n     * Test is {@code target} matches against any of the {@code predicates}.\n     * Used to easily target multiple LootTables\n     *\n     * @param target     The target identifier (this would be the current table)\n     * @param predicates The identifiers to test against (this would be the targeted tables)\n     * @return {@code true} if target matches any of the predicates\n     */\n    public static boolean anyMatch(Identifier target, Identifier... predicates) {\n        for (var predicate : predicates) if (target.equals(predicate)) return true;\n        return false;\n    }\n\n    @ApiStatus.Internal\n    public static void registerListener() {\n        LootTableEvents.MODIFY.register((key, tableBuilder, source, provider) -> {\n            ADDITIONS.forEach((identifiers, lootPoolEntrySupplier) -> {\n                if (anyMatch(key.identifier(), identifiers)) tableBuilder.withPool(LootPool.lootPool().with(lootPoolEntrySupplier.get()));\n            });\n        });\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ops/TextOps.java",
    "content": "package io.wispforest.owo.ops;\n\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.network.chat.contents.TranslatableContents;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * A collection of common operations\n * for working with and stylizing {@link Component}\n */\npublic final class TextOps {\n\n    private TextOps() {}\n\n    /**\n     * Appends the {@code text} onto the {@code prefix} without\n     * modifying the siblings of either one\n     *\n     * @param prefix The prefix\n     * @param text   The text to add onto the prefix\n     * @return The combined text\n     */\n    public static MutableComponent concat(Component prefix, Component text) {\n        return Component.empty().append(prefix).append(text);\n    }\n\n    /**\n     * Creates a new {@link Component} with the specified color\n     * already applied\n     *\n     * @param text  The text to create\n     * @param color The color to use in {@code RRGGBB} format\n     * @return The colored text, specifically a {@link net.minecraft.network.chat.contents.PlainTextContents}\n     */\n    public static MutableComponent withColor(String text, int color) {\n        return Component.literal(text).setStyle(Style.EMPTY.withColor(color));\n    }\n\n    /**\n     * Creates a new {@link Component} with the specified color\n     * already applied\n     *\n     * @param text  The text to create\n     * @param color The color to use in {@code RRGGBB} format\n     * @return The colored text, specifically a {@link TranslatableContents}\n     */\n    public static MutableComponent translateWithColor(String text, int color) {\n        return Component.translatable(text).setStyle(Style.EMPTY.withColor(color));\n    }\n\n    /**\n     * Applies multiple {@link ChatFormatting}s to the given String, with\n     * each one after the first one beginning on a {@code §} symbol.\n     * The amount of {@code §} symbols must equal the amount of\n     * supplied formattings - 1\n     *\n     * @param text       The text to format, with optional format delimiters\n     * @param formatting The formattings to apply\n     * @return The formatted text\n     */\n    public static MutableComponent withFormatting(String text, ChatFormatting... formatting) {\n        var textPieces = text.split(\"§\");\n        if (formatting.length != textPieces.length) return withColor(\"unmatched format specifiers - this is a bug\", 0xff007f);\n\n        var textBase = Component.literal(textPieces[0]).withStyle(formatting[0]);\n\n        for (int i = 1; i < textPieces.length; i++) {\n            textBase.append(Component.literal(textPieces[i]).withStyle(formatting[i]));\n        }\n\n        return textBase;\n    }\n\n    /**\n     * Applies multiple colors to the given String, with\n     * each one after the first one beginning on a {@code §} symbol.\n     * The amount of {@code §} symbols must equal the amount of\n     * supplied colors - 1\n     *\n     * @param text   The text to colorize, with optional color delimiters\n     * @param colors The colors to apply, in {@code RRGGBB} format\n     * @return The colorized text\n     * @see #color(ChatFormatting)\n     */\n    public static MutableComponent withColor(String text, int... colors) {\n        var textPieces = text.split(\"§\");\n        if (colors.length != textPieces.length) return withColor(\"unmatched color specifiers - this is a bug\", 0xff007f);\n\n        var textBase = withColor(textPieces[0], colors[0]);\n\n        for (int i = 1; i < textPieces.length; i++) {\n            textBase.append(withColor(textPieces[i], colors[i]));\n        }\n\n        return textBase;\n    }\n\n    /**\n     * Determine the width of the given iterable of texts,\n     * which is defined as the width of the widest text\n     * in the iterable\n     *\n     * @param renderer The text renderer responsible for rendering\n     *                 the text later on\n     * @param texts    The texts to check\n     * @return The width of the widest text in the collection\n     */\n    public static int width(Font renderer, Iterable<Component> texts) {\n        int width = 0;\n        for (var text : texts) width = Math.max(width, renderer.width(text));\n        return width;\n    }\n\n    /**\n     * Determine the width of the given iterable of texts,\n     * which is defined as the width of the widest text\n     * in the iterable\n     *\n     * @param renderer The text renderer responsible for rendering\n     *                 the text later on\n     * @param texts    The texts to check\n     * @return The width of the widest text in the collection\n     */\n    public static int widthOrdered(Font renderer, Iterable<FormattedCharSequence> texts) {\n        int width = 0;\n        for (var text : texts) width = Math.max(width, renderer.width(text));\n        return width;\n    }\n\n    /**\n     * @return The color value associated with the given formatting\n     * in {@code RRGGBB} format, or {@code 0} if there is none\n     */\n    public static int color(ChatFormatting formatting) {\n        return formatting.getColor() == null ? 0 : formatting.getColor();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/particles/ClientParticles.java",
    "content": "package io.wispforest.owo.particles;\n\nimport io.wispforest.owo.util.VectorRandomUtils;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.core.particles.ParticleOptions;\nimport net.minecraft.core.particles.ParticleTypes;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.phys.Vec3;\n\n/**\n * A wrapper for vanilla's terrible particle system that allows for easier\n * and more complex multi-particle operations\n */\n@Environment(EnvType.CLIENT)\npublic final class ClientParticles {\n\n    private static int particleCount = 1;\n    private static boolean persist = false;\n\n    private static Vec3 velocity = new Vec3(0, 0, 0);\n    private static boolean randomizeVelocity = false;\n    private static double randomVelocityScalar = 0;\n    private static Direction.Axis randomizationAxis = null;\n\n    private ClientParticles() {}\n\n    /**\n     * Marks the values set by {@link ClientParticles#setParticleCount(int)} and {@link ClientParticles#setVelocity(Vec3)} to be persistent\n     */\n    public static void persist() {\n        ClientParticles.persist = true;\n    }\n\n    /**\n     * How many particles to spawn per operation\n     * <br><b>\n     * Volatile unless {@link ClientParticles#persist()} is called before the next operation\n     * </b>\n     */\n    public static void setParticleCount(int particleCount) {\n        ClientParticles.particleCount = particleCount;\n    }\n\n    /**\n     * The velocity added to each spawned particle\n     * <br><b>\n     * Volatile unless {@link ClientParticles#persist()} is called before the next operation\n     * </b>\n     */\n    public static void setVelocity(Vec3 velocity) {\n        ClientParticles.velocity = velocity;\n    }\n\n    /**\n     * Makes the system use a random velocity for each particle\n     * <br><b>\n     * Volatile unless {@link ClientParticles#persist()} is called before the next operation\n     * </b>\n     *\n     * @param scalar The scalar to use for the generated velocities which\n     *               nominally range from -0.5 to 0.5 on each axis\n     */\n    public static void randomizeVelocity(double scalar) {\n        randomizeVelocity = true;\n        randomVelocityScalar = scalar;\n        randomizationAxis = null;\n    }\n\n    /**\n     * Makes the system use a random velocity for each particle\n     * <br><b>\n     * Volatile unless {@link ClientParticles#persist()} is called before the next operation\n     * </b>\n     *\n     * @param scalar The scalar to use for the generated velocities which\n     *               nominally range from -0.5 to 0.5 on each axis\n     * @param axis   The axis on which to apply random velocity\n     */\n    public static void randomizeVelocityOnAxis(double scalar, Direction.Axis axis) {\n        randomizeVelocity = true;\n        randomVelocityScalar = scalar;\n        randomizationAxis = axis;\n    }\n\n    /**\n     * Forces a reset of velocity and particleCount\n     */\n    public static void reset() {\n        persist = false;\n        clearState();\n    }\n\n    private static void clearState() {\n        if (persist) return;\n\n        particleCount = 1;\n        velocity = new Vec3(0, 0, 0);\n\n        randomizeVelocity = false;\n    }\n\n    private static void addParticle(ParticleOptions particle, Level world, Vec3 location) {\n        if (randomizeVelocity) {\n            if (randomizationAxis == null) {\n                velocity = VectorRandomUtils.getRandomOffset(world, Vec3.ZERO, randomVelocityScalar);\n            } else {\n                final var stopIt_getSomeHelp = (world.random.nextDouble() * 2 - 1) * randomVelocityScalar;\n                velocity = switch (randomizationAxis) {\n                    case X -> new Vec3(stopIt_getSomeHelp, 0, 0);\n                    case Y -> new Vec3(0, stopIt_getSomeHelp, 0);\n                    case Z -> new Vec3(0, 0, stopIt_getSomeHelp);\n                };\n            }\n        }\n\n        world.addParticle(particle, location.x, location.y, location.z, velocity.x, velocity.y, velocity.z);\n    }\n\n    /**\n     * Spawns particles with a maximum offset of {@code deviation} from the center of {@code pos}\n     *\n     * @param particle  The particle to spawn\n     * @param world     The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param pos       The block to center on\n     * @param deviation The maximum deviation from the center of pos\n     */\n    public static void spawnCenteredOnBlock(ParticleOptions particle, Level world, BlockPos pos, double deviation) {\n        Vec3 location;\n\n        for (int i = 0; i < particleCount; i++) {\n            location = VectorRandomUtils.getRandomCenteredOnBlock(world, pos, deviation);\n            addParticle(particle, world, location);\n        }\n\n        clearState();\n    }\n\n    /**\n     * Spawns particles randomly distributed within {@code pos}\n     *\n     * @param particle The particle to spawn\n     * @param world    The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param pos      The block to spawn particles in\n     */\n    public static void spawnWithinBlock(ParticleOptions particle, Level world, BlockPos pos) {\n        Vec3 location;\n\n        for (int i = 0; i < particleCount; i++) {\n            location = VectorRandomUtils.getRandomWithinBlock(world, pos);\n            addParticle(particle, world, location);\n        }\n\n        clearState();\n    }\n\n    /**\n     * Spawns particles with a maximum offset of {@code deviation} from {@code pos + offset}\n     *\n     * @param particle  The particle to spawn\n     * @param world     The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param pos       The base position\n     * @param offset    The offset from {@code pos}\n     * @param deviation The scalar for random distribution\n     */\n    public static void spawnWithOffsetFromBlock(ParticleOptions particle, Level world, BlockPos pos, Vec3 offset, double deviation) {\n        Vec3 location;\n        offset = offset.add(Vec3.atLowerCornerOf(pos));\n\n        for (int i = 0; i < particleCount; i++) {\n            location = VectorRandomUtils.getRandomOffset(world, offset, deviation);\n\n            addParticle(particle, world, location);\n        }\n\n        clearState();\n    }\n\n    /**\n     * Spawns particles at the given location with a maximum offset of {@code deviation}\n     *\n     * @param particle  The particle to spawn\n     * @param world     The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param pos       The base position\n     * @param deviation The scalar from random distribution\n     */\n    public static void spawn(ParticleOptions particle, Level world, Vec3 pos, double deviation) {\n        Vec3 location;\n\n        for (int i = 0; i < particleCount; i++) {\n            location = VectorRandomUtils.getRandomOffset(world, pos, deviation);\n            addParticle(particle, world, location);\n        }\n\n        clearState();\n    }\n\n    /**\n     * Spawns particles at the given location with a maximum offset of {@code deviation}\n     *\n     * @param particle   The particle to spawn\n     * @param world      The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param pos        The base position\n     * @param deviationX The scalar from random distribution on x\n     * @param deviationY The scalar from random distribution on y\n     * @param deviationZ The scalar from random distribution on z\n     */\n    public static void spawnPrecise(ParticleOptions particle, Level world, Vec3 pos, double deviationX, double deviationY, double deviationZ) {\n        Vec3 location;\n\n        for (int i = 0; i < particleCount; i++) {\n            location = VectorRandomUtils.getRandomOffsetSpecific(world, pos, deviationX, deviationY, deviationZ);\n            addParticle(particle, world, location);\n        }\n\n        clearState();\n    }\n\n    /**\n     * Spawns enchant particles travelling from origin to destination\n     *\n     * @param world       The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param origin      The origin of the particle stream\n     * @param destination The destination of the particle stream\n     * @param deviation   The scalar for random distribution around {@code origin}\n     */\n    public static void spawnEnchantParticles(Level world, Vec3 origin, Vec3 destination, float deviation) {\n\n        Vec3 location;\n        Vec3 particleVector = origin.subtract(destination);\n\n        for (int i = 0; i < particleCount; i++) {\n            location = VectorRandomUtils.getRandomOffset(world, particleVector, deviation);\n            world.addParticle(ParticleTypes.ENCHANT, destination.x, destination.y, destination.z, location.x, location.y, location.z);\n        }\n\n        clearState();\n    }\n\n    /**\n     * Spawns a particle at the given location with a lifetime of {@code maxAge}\n     *\n     * @param particleType The type of the particle to spawn\n     * @param pos          The position to spawn at\n     * @param maxAge       The maxAge to set for the spawned particle\n     */\n    @SuppressWarnings(\"ConstantConditions\")\n    public static <T extends ParticleOptions> void spawnWithMaxAge(T particleType, Vec3 pos, int maxAge) {\n        var particle = Minecraft.getInstance().particleEngine.createParticle(particleType, pos.x, pos.y, pos.z, velocity.x, velocity.y, velocity.z);\n        if (particle == null) {\n            return;\n        }\n\n        particle.setLifetime(maxAge);\n        clearState();\n    }\n\n    /**\n     * Spawns a line of particles going from {@code start} to {@code end}\n     *\n     * @param particle  The particle to spawn\n     * @param world     The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param start     The line's origin\n     * @param end       The line's end point\n     * @param deviation A random offset from the line that particles can have\n     */\n    public static void spawnLine(ParticleOptions particle, Level world, Vec3 start, Vec3 end, float deviation) {\n        spawnLineInner(particle, world, start, end, deviation);\n        clearState();\n    }\n\n    /**\n     * Spawns a cube outline starting at {@code origin} and expanding by {@code size} in positive\n     * direction on all axis\n     *\n     * @param particle  The particle to spawn\n     * @param world     The world to spawn the particles in, must be {@link net.minecraft.client.multiplayer.ClientLevel}\n     * @param origin    The cube's origin\n     * @param size      The cube's side length\n     * @param deviation A random offset from the line that particles can have\n     */\n    public static void spawnCubeOutline(ParticleOptions particle, Level world, Vec3 origin, float size, float deviation) {\n\n        spawnLineInner(particle, world, origin, origin.add(size, 0, 0), deviation);\n        spawnLineInner(particle, world, origin.add(size, 0, 0), origin.add(size, 0, size), deviation);\n\n        spawnLineInner(particle, world, origin, origin.add(0, 0, size), deviation);\n        spawnLineInner(particle, world, origin.add(0, 0, size), origin.add(size, 0, size), deviation);\n\n        origin = origin.add(0, size, 0);\n\n        spawnLineInner(particle, world, origin, origin.add(size, 0, 0), deviation);\n        spawnLineInner(particle, world, origin.add(size, 0, 0), origin.add(size, 0, size), deviation);\n\n        spawnLineInner(particle, world, origin, origin.add(0, 0, size), deviation);\n        spawnLineInner(particle, world, origin.add(0, 0, size), origin.add(size, 0, size), deviation);\n\n        spawnLineInner(particle, world, origin, origin.add(0, -size, 0), deviation);\n        spawnLineInner(particle, world, origin.add(size, 0, 0), origin.add(size, -size, 0), deviation);\n        spawnLineInner(particle, world, origin.add(0, 0, size), origin.add(0, -size, size), deviation);\n        spawnLineInner(particle, world, origin.add(size, 0, size), origin.add(size, -size, size), deviation);\n\n        clearState();\n    }\n\n    private static void spawnLineInner(ParticleOptions particle, Level world, Vec3 start, Vec3 end, float deviation) {\n        Vec3 increment = end.subtract(start).scale(1f / (float) particleCount);\n\n        for (int i = 0; i < particleCount; i++) {\n            start = VectorRandomUtils.getRandomOffset(world, start, deviation);\n            addParticle(particle, world, start);\n            start = start.add(increment);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/particles/systems/ParticleSystem.java",
    "content": "package io.wispforest.owo.particles.systems;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.owo.network.NetworkException;\nimport io.wispforest.owo.util.OwoFreezer;\nimport io.wispforest.owo.util.ServicesFrozenException;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.Nullable;\n\n/**\n * Represents a particle effect that can be played\n * at a position in a world <i>on both client and server</i>,\n * with some optional data attached.\n * <br>\n * To run this effect, call {@link #spawn(Level, Vec3, Object)}. If you call this\n * on the server, a command will be sent to the client to execute the system.\n * <b>Thus, it is important this is registered on both client and server</b>\n * <p>\n * In case your particle effect not required any additional data,\n * use {@link Void} as the data class and pass {@code null} to {@link #spawn(Level, Vec3, Object)}\n *\n * @param <T> The data class\n */\npublic class ParticleSystem<T> {\n\n    private final ParticleSystemController manager;\n\n    final Class<T> dataClass;\n    final int index;\n    final Endec<T> endec;\n    ParticleSystemExecutor<T> handler;\n\n    private final boolean permitsContextlessExecution;\n\n    ParticleSystem(ParticleSystemController manager, Class<T> dataClass, int index, Endec<T> endec, ParticleSystemExecutor<T> handler) {\n        OwoFreezer.checkRegister(\"Particle systems\");\n\n        this.manager = manager;\n        this.dataClass = dataClass;\n        this.index = index;\n        this.endec = endec;\n        this.handler = handler;\n\n        this.permitsContextlessExecution = dataClass == Void.class;\n    }\n\n    /**\n     * Sets the particle system's handler.\n     *\n     * @param handler the code that is run to actually display the particle system\n     * @throws NetworkException if this particle system already has a handler\n     */\n    public void setHandler(ParticleSystemExecutor<T> handler) {\n        if (OwoFreezer.isFrozen()) throw new ServicesFrozenException(\"Particle systems can only be changed during mod init\");\n        if (this.handler != null) throw new NetworkException(\"Particle system already has a handler\");\n\n        this.handler = handler;\n    }\n\n    /**\n     * Spawns, or displays, whichever term you prefer,\n     * this particle system in the given level at the\n     * given position and with the passed context data\n     *\n     * <p><b>{@code null} data is only allowed if the data class of this\n     * particle system is {@link Void}</b>\n     *\n     * @param level The level to execute in\n     * @param pos   The position to execute at\n     * @param data  The context to execute with\n     */\n    public void spawn(Level level, Vec3 pos, @Nullable T data) {\n        if (data == null && !permitsContextlessExecution) throw new IllegalStateException(\"This particle system does not permit 'null' data\");\n\n        if (level.isClientSide()) {\n            handler.executeParticleSystem(level, pos, data);\n        } else {\n            manager.sendPacket(this, (ServerLevel) level, pos, data);\n        }\n    }\n\n    /**\n     * Convenience wrapper for {@link #spawn(Level, Vec3, Object)}\n     * that always passes {@code null} data\n     *\n     * @param level The level to execute in\n     * @param pos   The position to execute at\n     */\n    public void spawn(Level level, Vec3 pos) {\n        spawn(level, pos, null);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/particles/systems/ParticleSystemController.java",
    "content": "package io.wispforest.owo.particles.systems;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.network.NetworkException;\nimport io.wispforest.owo.network.OwoHandshake;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.util.OwoFreezer;\nimport io.wispforest.owo.util.ReflectionUtils;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;\nimport net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;\nimport net.fabricmc.fabric.api.networking.v1.PlayerLookup;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.network.protocol.common.custom.CustomPacketPayload;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.level.ServerLevel;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * A controller object that manages and creates {@link ParticleSystem}s.\n * It is recommended to have one of these per mod.\n * <p>\n * To obtain a new particle system, call {@link #register(Class, Endec, ParticleSystemExecutor)}\n * with the system's context data class and handler function. <b>It is important\n * that this is done on both client and server, otherwise joining the server\n * will fail in a handshake error</b>\n */\npublic class ParticleSystemController {\n\n    @ApiStatus.Internal\n    public static final Map<Identifier, ParticleSystemController> REGISTERED_CONTROLLERS = new HashMap<>();\n\n    @ApiStatus.Internal\n    public final Int2ObjectMap<ParticleSystem<?>> systemsByIndex = new Int2ObjectOpenHashMap<>();\n\n    public final Identifier channelId;\n    private final CustomPacketPayload.Type<ParticleSystemPayload> payloadId;\n    private int maxIndex = 0;\n    private final String ownerClassName;\n\n    private final ReflectiveEndecBuilder builder;\n\n    /**\n     * Creates a new controller with the given ID. Duplicate controller IDs\n     * are not allowed - if there is a collision, the name of the\n     * class that previously registered the controller will be part of\n     * the exception. <b>This may be called at any stage during\n     * mod initialization</b>\n     *\n     * @param channelId The packet ID to use\n     */\n    public ParticleSystemController(Identifier channelId) {\n        OwoFreezer.checkRegister(\"Particle system controllers\");\n\n        this.builder = MinecraftEndecs.addDefaults(new ReflectiveEndecBuilder());\n\n        if (REGISTERED_CONTROLLERS.containsKey(channelId)) {\n            throw new IllegalStateException(\"Controller with id '\" + channelId + \"' was already registered from class '\" +\n                    REGISTERED_CONTROLLERS.get(channelId).ownerClassName + \"'\");\n        }\n\n        this.channelId = channelId;\n        this.payloadId = new CustomPacketPayload.Type<>(channelId);\n        this.ownerClassName = ReflectionUtils.getCallingClassName(2);\n\n        var instanceEndec = Endec.<ParticleSystemInstance<?>, Integer>dispatched(\n            index -> {\n                @SuppressWarnings(\"unchecked\")\n                var system = (ParticleSystem<Object>) systemsByIndex.get(index);\n                return system.endec.xmap(x -> new ParticleSystemInstance<>(system, x), x -> x.data);\n            },\n            instance -> instance.system.index,\n            Endec.VAR_INT\n        );\n        var endec = StructEndecBuilder.of(\n            MinecraftEndecs.VEC3.fieldOf(\"pos\", ParticleSystemPayload::pos),\n            instanceEndec.fieldOf(\"instance\", ParticleSystemPayload::instance),\n            (pos, instance) -> new ParticleSystemPayload(payloadId, pos, instance)\n        );\n\n        PayloadTypeRegistry.playS2C().register(payloadId, CodecUtils.toPacketCodec(endec));\n\n        OwoHandshake.enable();\n        OwoHandshake.requireHandshake();\n\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {\n            ClientPlayNetworking.registerGlobalReceiver(payloadId, new Client()::handler);\n        }\n\n        REGISTERED_CONTROLLERS.put(channelId, this);\n    }\n\n    public ReflectiveEndecBuilder endecBuilder() {\n        return this.builder;\n    }\n\n    /**\n     * Registers the given system executor with the given\n     * context data class, thereby creating a new system\n     *\n     * @param dataClass The class to use as context data\n     * @param executor  The code that is run to actually display the particle system\n     * @param <T>       The type of context data to use\n     * @return The created particle system\n     */\n    public <T> ParticleSystem<T> register(Class<T> dataClass, Endec<T> endec, ParticleSystemExecutor<T> executor) {\n        int index = maxIndex++;\n        var system = new ParticleSystem<>(this, dataClass, index, endec, executor);\n        systemsByIndex.put(index, system);\n        return system;\n    }\n\n    /**\n     * Shorthand for {{@link #register(Class, Endec, ParticleSystemExecutor)}} which creates the endec\n     * through {@link ReflectiveEndecBuilder#get(Class)}\n     */\n    public <T> ParticleSystem<T> register(Class<T> dataClass, ParticleSystemExecutor<T> executor) {\n        return this.register(dataClass, this.builder.get(dataClass), executor);\n    }\n\n    /**\n     * Registers the given system executor with the given\n     * context data class, thereby creating a new system\n     * <p>\n     * This method defers executor registration, so\n     * you must register the handler later in a client entrypoint.\n     *\n     * @param dataClass The class to use as context data\n     * @param <T>       The type of context data to use\n     * @return The created particle system\n     * @see ParticleSystem#setHandler(ParticleSystemExecutor)\n     */\n    public <T> ParticleSystem<T> registerDeferred(Class<T> dataClass, Endec<T> endec) {\n        int index = maxIndex++;\n        var system = new ParticleSystem<>(this, dataClass, index, endec, null);\n        systemsByIndex.put(index, system);\n        return system;\n    }\n\n    /**\n     * Shorthand for {{@link #registerDeferred(Class, Endec)}} which creates the endec\n     * through {@link ReflectiveEndecBuilder#get(Class)}\n     */\n    public <T> ParticleSystem<T> registerDeferred(Class<T> dataClass) {\n        return this.registerDeferred(dataClass, this.builder.get(dataClass));\n    }\n\n    <T> void sendPacket(ParticleSystem<T> particleSystem, ServerLevel level, Vec3 pos, T data) {\n        ParticleSystemPayload payload = new ParticleSystemPayload(payloadId, pos, new ParticleSystemInstance<>(particleSystem, data));\n\n        for (var player : PlayerLookup.tracking(level, BlockPos.containing(pos))) {\n            ServerPlayNetworking.send(player, payload);\n        }\n    }\n\n    private void verify() {\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {\n            for (ParticleSystem<?> system : systemsByIndex.values()) {\n                if (system.handler == null) {\n                    throw new NetworkException(\"Some particle systems of \" + channelId + \" don't have handlers registered\");\n                }\n            }\n        }\n    }\n\n    static {\n        OwoFreezer.registerFreezeCallback(() -> {\n            for (ParticleSystemController controller : REGISTERED_CONTROLLERS.values()) {\n                controller.verify();\n            }\n        });\n    }\n\n    private record ParticleSystemInstance<T>(ParticleSystem<T> system, T data) {\n        public void execute(Level level, Vec3 pos) {\n            system.handler.executeParticleSystem(level, pos, data);\n        }\n    }\n\n    private record ParticleSystemPayload(CustomPacketPayload.Type<ParticleSystemPayload> id, Vec3 pos, ParticleSystemInstance<?> instance) implements CustomPacketPayload {\n        @Override\n        public Type<? extends CustomPacketPayload> type() {\n            return id;\n        }\n    }\n\n    @Environment(EnvType.CLIENT)\n    private static class Client {\n        private void handler(ParticleSystemPayload payload, ClientPlayNetworking.Context context) {\n            payload.instance.execute(context.client().level, payload.pos);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/particles/systems/ParticleSystemExecutor.java",
    "content": "package io.wispforest.owo.particles.systems;\n\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.phys.Vec3;\n\npublic interface ParticleSystemExecutor<T> {\n    /**\n     * Called when particles should be displayed\n     * at the given position in the given level,\n     * with the given data as additional context\n     *\n     * @param level The level to display in\n     * @param pos   The position to display at\n     * @param data  The data to display with\n     */\n    void executeParticleSystem(Level level, Vec3 pos, T data);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/ComplexRegistryAction.java",
    "content": "package io.wispforest.owo.registration;\n\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.Identifier;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * An action to be executed by a {@link RegistryHelper} if and only if\n * all of it's required entries are present in that helper's registry\n *\n * @see ComplexRegistryAction.Builder#create(Runnable)\n */\npublic class ComplexRegistryAction {\n\n    private final List<Identifier> predicates;\n    private final Runnable action;\n\n    protected ComplexRegistryAction(List<Identifier> predicates, Runnable action) {\n        this.predicates = predicates;\n        this.action = action;\n    }\n\n    protected <T> boolean preCheck(Registry<T> registry) {\n        predicates.removeIf(registry::containsKey);\n        if (!predicates.isEmpty()) return false;\n\n        action.run();\n        return true;\n    }\n\n    protected boolean update(Identifier id, Collection<Runnable> actionList) {\n        predicates.remove(id);\n        if (!predicates.isEmpty()) return false;\n\n        actionList.add(action);\n        return true;\n    }\n\n    public static class Builder {\n\n        private final Runnable action;\n        private final List<Identifier> predicates;\n\n        private Builder(Runnable action) {\n            this.action = action;\n            this.predicates = new ArrayList<>();\n        }\n\n        /**\n         * Creates a new builder to link the provided action\n         * to a list of identifiers\n         *\n         * @param action The action to run once all identifiers are found in the targeted registry\n         * @see #entry(Identifier)\n         * @see #entries(Collection)\n         */\n        public static Builder create(Runnable action) {\n            return new Builder(action);\n        }\n\n        public Builder entry(Identifier id) {\n            this.predicates.add(id);\n            return this;\n        }\n\n        public Builder entries(Collection<Identifier> ids) {\n            this.predicates.addAll(ids);\n            return this;\n        }\n\n        /**\n         * Creates a registry action that can get run by a {@link RegistryHelper} once all the entries\n         * added via this builder are found in the target registry\n         *\n         * @return The built action\n         */\n        public ComplexRegistryAction build() {\n            if (predicates.isEmpty()) throw new IllegalStateException(\"Predicate list must not be empty\");\n            return new ComplexRegistryAction(predicates, action);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/RegistryHelper.java",
    "content": "package io.wispforest.owo.registration;\n\nimport net.fabricmc.fabric.api.event.registry.RegistryEntryAddedCallback;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\n/**\n * A simple helper to run code conditionally based on whether certain registry\n * entries are present or not. Use {@link #get(Registry)}\n * to obtain the instance for a given registry\n */\npublic final class RegistryHelper<T> {\n\n    private static final Map<Registry<?>, RegistryHelper<?>> INSTANCES = new HashMap<>();\n\n    private final Registry<T> registry;\n    private final Map<Identifier, Consumer<T>> actions = new HashMap<>();\n\n    private final List<ComplexRegistryAction> complexActions = new ArrayList<>();\n\n    /**\n     * Gets the {@link RegistryHelper} instance for the provided registry\n     *\n     * @param registry The target registry\n     * @return The helper for the targeted registry\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T> RegistryHelper<T> get(Registry<T> registry) {\n        return (RegistryHelper<T>) INSTANCES.computeIfAbsent(registry, objects -> new RegistryHelper<>(registry));\n    }\n\n    @ApiStatus.Internal\n    public RegistryHelper(Registry<T> registry) {\n        this.registry = registry;\n        RegistryEntryAddedCallback.event(registry).register((rawId, id, object) -> {\n            if (actions.containsKey(id)) {\n                actions.remove(id).accept(object);\n            }\n\n            final var actionsToExecute = new ArrayList<Runnable>();\n            complexActions.removeIf(action -> action.update(id, actionsToExecute));\n            actionsToExecute.forEach(Runnable::run);\n        });\n    }\n\n    /**\n     * Runs the given consumer supplied with the registered object as soon\n     * as the requested ID exists in the registry\n     *\n     * @param id     The ID the registry must contain for {@code action} to be run\n     * @param action The code to run once {@code id} is present\n     */\n    public void runWhenPresent(Identifier id, Consumer<T> action) {\n        if (isContained(registry, id)) {\n            action.accept(registry.getValue(id));\n        } else {\n            this.actions.put(id, action);\n        }\n    }\n\n    /**\n     * Runs the given action once all of its required entries are\n     * present in the registry\n     *\n     * @param action The {@link ComplexRegistryAction} to run or queue\n     */\n    public void runWhenPresent(ComplexRegistryAction action) {\n        if (!action.preCheck(registry)) {\n            this.complexActions.add(action);\n        }\n    }\n\n    private static <T> boolean isContained(Registry<T> registry, Identifier identifier) {\n        return registry.containsKey(identifier);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/annotations/AssignedName.java",
    "content": "package io.wispforest.owo.registration.annotations;\n\nimport io.wispforest.owo.registration.reflect.FieldRegistrationHandler;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Declares the name the targeted field should be assigned when processed by\n * {@link FieldRegistrationHandler}\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface AssignedName {\n    String value();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/annotations/IterationIgnored.java",
    "content": "package io.wispforest.owo.registration.annotations;\n\nimport io.wispforest.owo.registration.reflect.FieldRegistrationHandler;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Specifies that the target field should be ignored by all operations\n * of {@link FieldRegistrationHandler}\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface IterationIgnored {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/annotations/RegistryNamespace.java",
    "content": "package io.wispforest.owo.registration.annotations;\n\nimport io.wispforest.owo.registration.reflect.AutoRegistryContainer;\nimport io.wispforest.owo.registration.reflect.FieldRegistrationHandler;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Can be used to override the namespace an implementation of\n * {@link AutoRegistryContainer} uses.\n * <p>\n * This only applies to inner classes, top level classes have their namespace defined\n * in the call to {@link FieldRegistrationHandler}\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface RegistryNamespace {\n    String value();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/reflect/AutoRegistryContainer.java",
    "content": "package io.wispforest.owo.registration.reflect;\n\nimport io.wispforest.owo.registration.annotations.AssignedName;\nimport net.minecraft.core.Registry;\n\nimport java.lang.reflect.Field;\n\n/**\n * A special version of {@link FieldProcessingSubject} that contains fields which should\n * be registered into a {@link Registry} using the field names in lowercase as ID\n * <p>\n * Use {@link #register(Class, String, boolean)} to automatically register all fields\n * of a given implementation into its specified registry\n *\n * @param <T> The type of objects to register, same as the Registry's type parameter\n */\npublic interface AutoRegistryContainer<T> extends FieldProcessingSubject<T> {\n\n    /**\n     * @return The registry the fields of this class should be registered into\n     */\n    Registry<T> getRegistry();\n\n    /**\n     * Called after the given field has been registered\n     *\n     * @param namespace  The namespace that is being used to register this class' fields\n     * @param value      The value that was registered\n     * @param identifier The identifier the field was assigned, possibly overridden by an {@link AssignedName}\n     *                   annotation and always fully lowercase\n     */\n    default void postProcessField(String namespace, T value, String identifier, Field field) {}\n\n    /**\n     * Convenience-alias for {@link FieldRegistrationHandler#register(Class, String, boolean)}\n     */\n    static <T> void register(Class<? extends AutoRegistryContainer<T>> container, String namespace, boolean recurse) {\n        FieldRegistrationHandler.register(container, namespace, recurse);\n    }\n\n    @SuppressWarnings({\"unchecked\"})\n    static <T> Class<T> conform(Class<?> input) {\n        return (Class<T>) input;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/reflect/BlockEntityRegistryContainer.java",
    "content": "package io.wispforest.owo.registration.reflect;\n\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\n\npublic interface BlockEntityRegistryContainer extends AutoRegistryContainer<BlockEntityType<?>> {\n\n    @Override\n    default Registry<BlockEntityType<?>> getRegistry() {\n        return BuiltInRegistries.BLOCK_ENTITY_TYPE;\n    }\n\n    @Override\n    default Class<BlockEntityType<?>> getTargetFieldType() {\n        return AutoRegistryContainer.conform(BlockEntityType.class);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/reflect/FieldProcessingSubject.java",
    "content": "package io.wispforest.owo.registration.reflect;\n\nimport io.wispforest.owo.registration.annotations.AssignedName;\n\nimport java.lang.reflect.Field;\n\n/**\n * A class that can have its accessible static fields that match the\n * class of <b>T</b> processed by the {@link FieldRegistrationHandler}\n * <p>\n * <b>All implementations must provide a zero-args constructor</b>\n *\n * @param <T> The type of field to be processed\n */\npublic interface FieldProcessingSubject<T> {\n\n    /**\n     * @return The class of <b>T</b>\n     */\n    Class<T> getTargetFieldType();\n\n    /**\n     * Called to check if a given field should be processed\n     *\n     * @param value      The value the inspected field currently has\n     * @param identifier The identifier that field was assigned, possibly overridden by an {@link AssignedName}\n     *                   annotation and always fully lowercase\n     * @return {@code true} if the inspected field should be processed\n     */\n    default boolean shouldProcessField(T value, String identifier, Field field) {\n        return true;\n    }\n\n    /**\n     * Called after all applicable fields of this class have been processed\n     */\n    default void afterFieldProcessing() {\n\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/reflect/FieldRegistrationHandler.java",
    "content": "package io.wispforest.owo.registration.reflect;\n\nimport io.wispforest.owo.registration.annotations.RegistryNamespace;\nimport io.wispforest.owo.util.ReflectionUtils;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.Identifier;\n\n/**\n * Main hub for all interactions with implementations of\n * {@link FieldProcessingSubject}\n */\n@SuppressWarnings(\"unchecked\")\npublic final class FieldRegistrationHandler {\n\n    private FieldRegistrationHandler() {}\n\n    /**\n     * Applies the given processor to all applicable fields of the targeted class\n     *\n     * @param clazz                   The class to target, must implement {@link FieldProcessingSubject}\n     * @param processor               The function to apply to each applicable field\n     * @param recurseIntoInnerClasses Whether this method should recursively process all inner classes of {@code clazz}\n     * @param <T>                     The type of field to match\n     */\n    public static <T> void process(Class<? extends FieldProcessingSubject<T>> clazz, ReflectionUtils.FieldConsumer<T> processor, boolean recurseIntoInnerClasses) {\n        var handler = ReflectionUtils.tryInstantiateWithNoArgs(clazz);\n        ReflectionUtils.iterateAccessibleStaticFields(clazz, handler.getTargetFieldType(), createProcessor(processor, handler));\n\n        if (recurseIntoInnerClasses) {\n            ReflectionUtils.forApplicableSubclasses(clazz, FieldProcessingSubject.class,\n                    subclass -> process((Class<? extends FieldProcessingSubject<T>>) subclass, processor, true));\n        }\n\n        handler.afterFieldProcessing();\n    }\n\n    /**\n     * Processes all fields of the given class with the implementation of\n     * {@code processField(T, String)} it provides\n     *\n     * @param clazz                   The class to target, must implement {@link SimpleFieldProcessingSubject}\n     * @param recurseIntoInnerClasses Whether this method should recursively process all inner classes of {@code clazz}\n     * @param <T>                     The type of field to match\n     */\n    public static <T> void processSimple(Class<? extends SimpleFieldProcessingSubject<T>> clazz, boolean recurseIntoInnerClasses) {\n        var handler = ReflectionUtils.tryInstantiateWithNoArgs(clazz);\n        ReflectionUtils.iterateAccessibleStaticFields(clazz, handler.getTargetFieldType(), createProcessor(handler::processField, handler));\n\n        if (recurseIntoInnerClasses) {\n            ReflectionUtils.forApplicableSubclasses(clazz, SimpleFieldProcessingSubject.class,\n                    subclass -> processSimple((Class<? extends SimpleFieldProcessingSubject<T>>) subclass, true));\n        }\n\n        handler.afterFieldProcessing();\n    }\n\n    /**\n     * Registers all public static fields of the specified class that\n     * match its type parameter into the registry it specifies\n     *\n     * @param clazz     The class from which to take the fields, must implement {@link AutoRegistryContainer}\n     * @param namespace The namespace to use in the generated identifiers\n     * @param <T>       The type of object to register\n     */\n    public static <T> void register(Class<? extends AutoRegistryContainer<T>> clazz, String namespace, boolean recurseIntoInnerClasses) {\n        AutoRegistryContainer<T> container = ReflectionUtils.tryInstantiateWithNoArgs(clazz);\n\n        ReflectionUtils.iterateAccessibleStaticFields(clazz, container.getTargetFieldType(), createProcessor((fieldValue, identifier, field) -> {\n            Registry.register(container.getRegistry(), Identifier.fromNamespaceAndPath(namespace, identifier), fieldValue);\n            container.postProcessField(namespace, fieldValue, identifier, field);\n        }, container));\n\n        if (recurseIntoInnerClasses) {\n            ReflectionUtils.forApplicableSubclasses(clazz, AutoRegistryContainer.class, subclass -> {\n                var classModId = namespace;\n                if (subclass.isAnnotationPresent(RegistryNamespace.class)) classModId = subclass.getAnnotation(RegistryNamespace.class).value();\n                register((Class<? extends AutoRegistryContainer<T>>) subclass, classModId, true);\n            });\n        }\n\n        container.afterFieldProcessing();\n    }\n\n    private static <T> ReflectionUtils.FieldConsumer<T> createProcessor(ReflectionUtils.FieldConsumer<T> delegate, FieldProcessingSubject<T> handler) {\n        return (value, name, field) -> {\n            if (!handler.shouldProcessField(value, name, field)) return;\n            delegate.accept(value, name, field);\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/registration/reflect/SimpleFieldProcessingSubject.java",
    "content": "package io.wispforest.owo.registration.reflect;\n\nimport io.wispforest.owo.registration.annotations.AssignedName;\n\nimport java.lang.reflect.Field;\n\n/**\n * A simpler to use version of {@link FieldProcessingSubject} that\n * provides the processor to apply to its fields\n *\n * @param <T>\n */\npublic interface SimpleFieldProcessingSubject<T> extends FieldProcessingSubject<T> {\n\n    /**\n     * Processes the given field\n     *\n     * @param value      The value of the inspected field at the time this method is called\n     * @param identifier The identifier that field was assigned, either it's name in lowercase or specified\n     *                   by an {@link AssignedName} annotation\n     */\n    void processField(T value, String identifier, Field field);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/renderdoc/RenderDoc.java",
    "content": "package io.wispforest.owo.renderdoc;\n\nimport com.sun.jna.Library;\nimport com.sun.jna.Native;\nimport com.sun.jna.ptr.IntByReference;\nimport com.sun.jna.ptr.LongByReference;\nimport com.sun.jna.ptr.PointerByReference;\nimport io.wispforest.owo.Owo;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectMap;\nimport it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;\nimport net.minecraft.util.Util;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\nimport org.lwjgl.system.linux.DynamicLinkLoader;\n\nimport java.time.Instant;\nimport java.util.Arrays;\nimport java.util.EnumSet;\nimport java.util.Map;\n\n@ApiStatus.Experimental\n@SuppressWarnings({\"unused\", \"UnusedReturnValue\"})\npublic final class RenderDoc {\n\n    private RenderDoc() {}\n\n    private static final RenderdocLibrary.RenderdocApi renderdoc;\n\n    static {\n        var apiPointer = new PointerByReference();\n        RenderdocLibrary.RenderdocApi apiInstance = null;\n\n        var os = Util.getPlatform();\n\n        if (os == Util.OS.WINDOWS || os == Util.OS.LINUX) {\n            try {\n                RenderdocLibrary renderdocLibrary;\n                if (os == Util.OS.WINDOWS) {\n                    renderdocLibrary = Native.load(\"renderdoc\", RenderdocLibrary.class);\n                } else {\n                    int flags = DynamicLinkLoader.RTLD_NOW | DynamicLinkLoader.RTLD_NOLOAD;\n                    if (DynamicLinkLoader.dlopen(\"librenderdoc.so\", flags) == 0) {\n                        throw new UnsatisfiedLinkError();\n                    }\n\n                    renderdocLibrary = Native.load(\"renderdoc\", RenderdocLibrary.class, Map.of(Library.OPTION_OPEN_FLAGS, flags));\n                }\n\n                int initResult = renderdocLibrary.RENDERDOC_GetAPI(10500, apiPointer);\n                if (initResult != 1) {\n                    Owo.LOGGER.error(\"Could not connect to RenderDoc API, return code: {}\", initResult);\n                } else {\n                    apiInstance = new RenderdocLibrary.RenderdocApi(apiPointer.getValue());\n\n                    var major = new IntByReference();\n                    var minor = new IntByReference();\n                    var patch = new IntByReference();\n                    apiInstance.GetAPIVersion.call(major, minor, patch);\n                    Owo.LOGGER.info(\"Connected to RenderDoc API v\" + major.getValue() + \".\" + minor.getValue() + \".\" + patch.getValue());\n                }\n            } catch (UnsatisfiedLinkError ignored) {}\n        }\n\n        renderdoc = apiInstance;\n    }\n\n    /**\n     * @return {@code true} if the RenderDoc dynamic library is loaded\n     * and owo has successfully connected to the API\n     */\n    public static boolean isAvailable() {\n        return renderdoc != null;\n    }\n\n    /**\n     * @return The version of the RenderDoc API that owo is connected to,\n     * in &lt;major&gt;.&lt;minor&gt;.&lt;patch&gt; semver format\n     */\n    public static String getAPIVersion() {\n        if (renderdoc == null) return \"not connected\";\n\n        var major = new IntByReference();\n        var minor = new IntByReference();\n        var patch = new IntByReference();\n        renderdoc.GetAPIVersion.call(major, minor, patch);\n\n        return major.getValue() + \".\" + minor.getValue() + \".\" + patch.getValue();\n    }\n\n    /**\n     * Set the value of a RenderDoc capture option\n     *\n     * @param option The option to modify\n     * @param value  The value to change the option to\n     * @return {@code true} if the value was correct and the option\n     * was successfully modified\n     */\n    public static <T> boolean setCaptureOption(CaptureOption<T> option, T value) {\n        if (renderdoc == null) return false;\n\n        if (value instanceof Boolean bool) {\n            return renderdoc.SetCaptureOptionU32.call(option.idx, new RenderdocLibrary.uint32_t(bool ? 1 : 0)) == 1;\n        } else if (value instanceof Integer uint) {\n            return renderdoc.SetCaptureOptionU32.call(option.idx, new RenderdocLibrary.uint32_t(uint)) == 1;\n        } else {\n            throw new UnsupportedOperationException();\n        }\n    }\n\n    /**\n     * Get the value of a RenderDoc capture option\n     *\n     * @param option The option to query\n     * @return The current value of the option\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T getCaptureOption(CaptureOption<T> option) {\n        if (renderdoc == null) return null;\n\n        if (option.type == Boolean.class) {\n            return (T) Boolean.valueOf(renderdoc.GetCaptureOptionU32.call(option.idx).intValue() == 1);\n        } else if (option.type == Integer.class) {\n            return (T) Integer.valueOf(renderdoc.GetCaptureOptionU32.call(option.idx).intValue());\n        } else {\n            throw new UnsupportedOperationException();\n        }\n    }\n\n    /**\n     * Set the hotkeys used to trigger a capture\n     */\n    public static void setCaptureKeys(Key... keys) {\n        if (renderdoc == null) return;\n        renderdoc.SetCaptureKeys.call(Arrays.stream(keys).mapToInt(value -> value.keycode).toArray(), keys.length);\n    }\n\n    /**\n     * Query the current configuration of the RenderDoc overlay\n     *\n     * @return All parts of the overlay which are currently enabled\n     */\n    public static EnumSet<OverlayOption> getOverlayOptions() {\n        if (renderdoc == null) return null;\n\n        int mask = renderdoc.GetOverlayBits.call().intValue();\n\n        var set = EnumSet.noneOf(OverlayOption.class);\n        for (var option : OverlayOption.values()) {\n            if ((mask & option.mask) != 0) set.add(option);\n        }\n\n        return set;\n    }\n\n    /**\n     * Enable some parts of the RenderDoc overlay\n     *\n     * @param options The options to enable\n     */\n    public static void enableOverlayOptions(OverlayOption... options) {\n        if (renderdoc == null) return;\n\n        int mask = 0;\n        for (var option : options) mask |= option.mask;\n\n        renderdoc.MaskOverlayBits.call(new RenderdocLibrary.uint32_t(~0), new RenderdocLibrary.uint32_t(mask));\n    }\n\n    /**\n     * Disable some parts of the RenderDoc overlay\n     *\n     * @param options The options to enable\n     */\n    public static void disableOverlayOptions(OverlayOption... options) {\n        if (renderdoc == null) return;\n\n        int mask = 0;\n        for (var option : options) mask |= option.mask;\n\n        renderdoc.MaskOverlayBits.call(new RenderdocLibrary.uint32_t(~mask), new RenderdocLibrary.uint32_t(0));\n    }\n\n    /**\n     * Try to remove all RenderDoc hooks from the process. If this\n     * is called after a graphics API has been initialized, behavior\n     * is undefined\n     */\n    public static void removeHooks() {\n        if (renderdoc == null) return;\n        renderdoc.RemoveHooks.call();\n    }\n\n    /**\n     * Remove RenderDoc's crash handler from the process\n     */\n    public static void unloadCrashHandler() {\n        if (renderdoc == null) return;\n        renderdoc.UnloadCrashHandler.call();\n    }\n\n    /**\n     * Set the template used to generate new capture file names\n     */\n    public static void setCaptureFilePathTemplate(String template) {\n        if (renderdoc == null) return;\n        renderdoc.SetCaptureFilePathTemplate.call(template);\n    }\n\n    /**\n     * @return the template used to generate new capture file names\n     */\n    public static String getCaptureFilePathTemplate() {\n        if (renderdoc == null) return null;\n        return renderdoc.GetCaptureFilePathTemplate.call();\n    }\n\n    /**\n     * Query information about a specific capture\n     *\n     * @param index The index to query\n     * @return The path and timestamp of the capture at the given index,\n     * or {@code null} if no such capture exists\n     */\n    public static Capture getCapture(int index) {\n        if (renderdoc == null) return null;\n\n        var length = new IntByReference();\n        if (renderdoc.GetCapture.call(index, null, length, null).intValue() != 1) {\n            return null;\n        }\n\n        var filename = new byte[length.getValue()];\n        var timestamp = new LongByReference();\n\n        renderdoc.GetCapture.call(index, filename, length, timestamp);\n        return new Capture(new String(filename, 0, filename.length - 1), Instant.ofEpochSecond(timestamp.getValue()));\n    }\n\n    /**\n     * @return How many captures have been made\n     */\n    public static int getNumCaptures() {\n        if (renderdoc == null) return -1;\n        return renderdoc.GetNumCaptures.call().intValue();\n    }\n\n    /**\n     * Trigger a capture of the next frame, as\n     * if the user had pressed on the capture hotkeys\n     */\n    public static void triggerCapture() {\n        if (renderdoc == null) return;\n        renderdoc.TriggerCapture.call();\n    }\n\n    /**\n     * Immediately begin a capture\n     */\n    public static void startFrameCapture() {\n        if (renderdoc == null) return;\n        renderdoc.StartFrameCapture.call(null, null);\n    }\n\n    /**\n     * @return {@code true} if a capture is currently being performed\n     */\n    public static boolean isFrameCapturing() {\n        if (renderdoc == null) return false;\n        return renderdoc.IsFrameCapturing.call().intValue() == 1;\n    }\n\n    /**\n     * Immediately end an active capture\n     */\n    public static void endFrameCapture() {\n        if (renderdoc == null) return;\n        renderdoc.EndFrameCapture.call(null, null);\n    }\n\n    /**\n     * @return {@code true} if a RenderDoc replay UI\n     * instance is currently attached to this process\n     */\n    public static boolean isReplayUIConnected() {\n        if (renderdoc == null) return false;\n        return renderdoc.IsTargetControlConnected.call().intValue() == 1;\n    }\n\n    /**\n     * Open the RenderDoc replay UI\n     *\n     * @param connect {@code true} if the new UI instance should instantly\n     *                attach to this process\n     * @return The PID of the spawned process, or {@code 0} if the UI could not be opened\n     */\n    public static int launchReplayUI(boolean connect) {\n        if (renderdoc == null) return -1;\n        return renderdoc.LaunchReplayUI.call(new RenderdocLibrary.uint32_t(connect ? 1 : 0), null).intValue();\n    }\n\n    /**\n     * Request the currently connected replay UI to raise\n     * its window to the top - this is not guaranteed to work on every OS\n     *\n     * @return {@code true} if the UI tried to raise its window, {@code false}\n     * if some error occurred while passing on the command or no UI is connected\n     */\n    public static boolean showReplayUI() {\n        if (renderdoc == null) return false;\n        return renderdoc.ShowReplayUI.call().intValue() == 1;\n    }\n\n    /**\n     * Set the comments attached to a specific capture\n     *\n     * @param capture  The capture to modify, obtain with {@link #getCapture(int)}\n     * @param comments The new capture comments\n     */\n    public static void setCaptureComments(Capture capture, String comments) {\n        if (renderdoc == null) return;\n        renderdoc.SetCaptureFileComments.call(capture.path, comments);\n    }\n\n    public static final class CaptureOption<T> {\n        public static final CaptureOption<Boolean> ALLOW_VSYNC = new CaptureOption<>(0, Boolean.class);\n        public static final CaptureOption<Boolean> ALLOW_FULLSCREEN = new CaptureOption<>(1, Boolean.class);\n        public static final CaptureOption<Boolean> API_VALIDATION = new CaptureOption<>(2, Boolean.class);\n        public static final CaptureOption<Boolean> CAPTURE_CALLSTACKS = new CaptureOption<>(3, Boolean.class);\n        public static final CaptureOption<Boolean> CAPTURE_CALLSTACKS_ONLY_DRAWS = new CaptureOption<>(4, Boolean.class);\n        public static final CaptureOption<Integer> DELAY_FOR_DEBUGGER = new CaptureOption<>(5, Integer.class);\n        public static final CaptureOption<Boolean> VERIFY_BUFFER_ACCESS = new CaptureOption<>(6, Boolean.class);\n        public static final CaptureOption<Boolean> HOOK_INTO_CHILDREN = new CaptureOption<>(7, Boolean.class);\n        public static final CaptureOption<Boolean> REF_ALL_RESOURCES = new CaptureOption<>(8, Boolean.class);\n        public static final CaptureOption<Boolean> SAVE_ALL_INITIALS = new CaptureOption<>(9, Boolean.class);\n        public static final CaptureOption<Boolean> CAPTURE_ALL_CMD_LISTS = new CaptureOption<>(10, Boolean.class);\n        public static final CaptureOption<Boolean> DEBUG_OUTPUT_MUTE = new CaptureOption<>(11, Boolean.class);\n\n        @Deprecated\n        public static final CaptureOption<?> ALLOW_UNSUPPORTED_VENDOR_EXTENSIONS = new CaptureOption<>(12, Void.class);\n\n        public final int idx;\n        private final Class<T> type;\n\n        CaptureOption(int idx, Class<T> type) {\n            this.idx = idx;\n            this.type = type;\n        }\n    }\n\n    public enum Key {\n        // '0' - '9' matches ASCII values\n        ZERO(0x30, GLFW.GLFW_KEY_0),\n        ONE(0x31, GLFW.GLFW_KEY_1),\n        TWO(0x32, GLFW.GLFW_KEY_2),\n        THREE(0x33, GLFW.GLFW_KEY_2),\n        FOUR(0x34, GLFW.GLFW_KEY_4),\n        FIVE(0x35, GLFW.GLFW_KEY_5),\n        SIX(0x36, GLFW.GLFW_KEY_6),\n        SEVEN(0x37, GLFW.GLFW_KEY_7),\n        EIGHT(0x38, GLFW.GLFW_KEY_8),\n        NINE(0x39, GLFW.GLFW_KEY_9),\n\n        // 'A' - 'Z' matches ASCII values\n        A(0x41, GLFW.GLFW_KEY_A),\n        B(0x42, GLFW.GLFW_KEY_B),\n        C(0x43, GLFW.GLFW_KEY_C),\n        D(0x44, GLFW.GLFW_KEY_D),\n        E(0x45, GLFW.GLFW_KEY_E),\n        F(0x46, GLFW.GLFW_KEY_F),\n        G(0x47, GLFW.GLFW_KEY_G),\n        H(0x48, GLFW.GLFW_KEY_H),\n        I(0x49, GLFW.GLFW_KEY_I),\n        J(0x4A, GLFW.GLFW_KEY_J),\n        K(0x4B, GLFW.GLFW_KEY_K),\n        L(0x4C, GLFW.GLFW_KEY_L),\n        M(0x4D, GLFW.GLFW_KEY_M),\n        N(0x4E, GLFW.GLFW_KEY_N),\n        O(0x4F, GLFW.GLFW_KEY_O),\n        P(0x50, GLFW.GLFW_KEY_P),\n        Q(0x51, GLFW.GLFW_KEY_Q),\n        R(0x52, GLFW.GLFW_KEY_R),\n        S(0x53, GLFW.GLFW_KEY_S),\n        T(0x54, GLFW.GLFW_KEY_T),\n        U(0x55, GLFW.GLFW_KEY_U),\n        V(0x56, GLFW.GLFW_KEY_V),\n        W(0x57, GLFW.GLFW_KEY_W),\n        X(0x58, GLFW.GLFW_KEY_X),\n        Y(0x59, GLFW.GLFW_KEY_Y),\n        Z(0x5A, GLFW.GLFW_KEY_Z),\n\n        // leave the rest of the ASCII range free\n        // in case we want to use it later\n        NON_PRINTABLE(0x100, -1),\n\n        DIVIDE(0x101, GLFW.GLFW_KEY_KP_DIVIDE),\n        MULTIPLY(0x102, GLFW.GLFW_KEY_KP_MULTIPLY),\n        SUBTRACT(0x103, GLFW.GLFW_KEY_KP_SUBTRACT),\n        PLUS(0x104, GLFW.GLFW_KEY_KP_ADD),\n\n        F1(0x105, GLFW.GLFW_KEY_F1),\n        F2(0x106, GLFW.GLFW_KEY_F2),\n        F3(0x107, GLFW.GLFW_KEY_F3),\n        F4(0x108, GLFW.GLFW_KEY_F4),\n        F5(0x109, GLFW.GLFW_KEY_F5),\n        F6(0x10a, GLFW.GLFW_KEY_F6),\n        F7(0x10b, GLFW.GLFW_KEY_F7),\n        F8(0x10c, GLFW.GLFW_KEY_F8),\n        F9(0x10d, GLFW.GLFW_KEY_F9),\n        F10(0x10e, GLFW.GLFW_KEY_F10),\n        F11(0x10f, GLFW.GLFW_KEY_F11),\n        F12(0x110, GLFW.GLFW_KEY_F12),\n\n        HOME(0x111, GLFW.GLFW_KEY_HOME),\n        END(0x112, GLFW.GLFW_KEY_END),\n        INSERT(0x113, GLFW.GLFW_KEY_INSERT),\n        DELETE(0x114, GLFW.GLFW_KEY_DELETE),\n        PAGE_UP(0x115, GLFW.GLFW_KEY_PAGE_UP),\n        PAGE_DOWN(0x116, GLFW.GLFW_KEY_PAGE_DOWN),\n\n        BACKSPACE(0x117, GLFW.GLFW_KEY_BACKSPACE),\n        TAB(0x118, GLFW.GLFW_KEY_TAB),\n        PRINT_SCREEN(0x119, GLFW.GLFW_KEY_PRINT_SCREEN),\n        PAUSE(0x11a, GLFW.GLFW_KEY_PAUSE);\n\n        private final int keycode;\n        private final int glfw;\n\n        Key(int keycode, int glfw) {\n            this.keycode = keycode;\n            this.glfw = glfw;\n        }\n\n        private static final Int2ObjectMap<Key> GLFW_MAPPINGS = new Int2ObjectOpenHashMap<>();\n\n        public static @Nullable Key fromGLFW(int glfw) {\n            return GLFW_MAPPINGS.getOrDefault(glfw, null);\n        }\n\n        static {\n            for (var key : values()) {\n                if (key.glfw < 0) continue;\n                GLFW_MAPPINGS.put(key.glfw, key);\n            }\n        }\n    }\n\n    public enum OverlayOption {\n        ENABLED(0x1),\n        FRAME_RATE(0x2),\n        FRAME_NUMBER(0x4),\n        CAPTURE_LIST(0x8),\n        DEFAULT(ENABLED.mask | FRAME_RATE.mask | FRAME_NUMBER.mask | CAPTURE_LIST.mask),\n        ALL(~0),\n        NONE(0);\n\n        public final int mask;\n\n        OverlayOption(int mask) {\n            this.mask = mask;\n        }\n    }\n\n    public record Capture(String path, Instant timestamp) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/renderdoc/RenderdocLibrary.java",
    "content": "package io.wispforest.owo.renderdoc;\n\nimport com.sun.jna.*;\nimport com.sun.jna.ptr.IntByReference;\nimport com.sun.jna.ptr.LongByReference;\nimport com.sun.jna.ptr.PointerByReference;\n\ninterface RenderdocLibrary extends Library {\n\n    int RENDERDOC_GetAPI(int version, PointerByReference out);\n\n    @SuppressWarnings(\"unused\")\n    @Structure.FieldOrder({\n            \"GetAPIVersion\", \"SetCaptureOptionU32\", \"SetCaptureOptionF32\", \"GetCaptureOptionU32\", \"GetCaptureOptionF32\", \"SetFocusToggleKeys\",\n            \"SetCaptureKeys\", \"GetOverlayBits\", \"MaskOverlayBits\", \"RemoveHooks\", \"UnloadCrashHandler\", \"SetCaptureFilePathTemplate\", \"GetCaptureFilePathTemplate\",\n            \"GetNumCaptures\", \"GetCapture\", \"TriggerCapture\", \"IsTargetControlConnected\", \"LaunchReplayUI\", \"SetActiveWindow\", \"StartFrameCapture\",\n            \"IsFrameCapturing\", \"EndFrameCapture\", \"TriggerMultiFrameCapture\", \"SetCaptureFileComments\", \"DiscardFrameCapture\", \"ShowReplayUI\"\n    })\n    class RenderdocApi extends Structure {\n\n        public pRENDERDOC_GetAPIVersion GetAPIVersion;\n        public pRENDERDOC_SetCaptureOptionU32 SetCaptureOptionU32;\n        public pRENDERDOC_SetCaptureOptionF32 SetCaptureOptionF32;\n        public pRENDERDOC_GetCaptureOptionU32 GetCaptureOptionU32;\n        public pRENDERDOC_GetCaptureOptionF32 GetCaptureOptionF32;\n        public pRENDERDOC_SetFocusToggleKeys SetFocusToggleKeys;\n        public pRENDERDOC_SetCaptureKeys SetCaptureKeys;\n        public pRENDERDOC_GetOverlayBits GetOverlayBits;\n        public pRENDERDOC_MaskOverlayBits MaskOverlayBits;\n        public pRENDERDOC_RemoveHooks RemoveHooks;\n        public pRENDERDOC_UnloadCrashHandler UnloadCrashHandler;\n        public pRENDERDOC_SetCaptureFilePathTemplate SetCaptureFilePathTemplate;\n        public pRENDERDOC_GetCaptureFilePathTemplate GetCaptureFilePathTemplate;\n        public pRENDERDOC_GetNumCaptures GetNumCaptures;\n        public pRENDERDOC_GetCapture GetCapture;\n        public pRENDERDOC_TriggerCapture TriggerCapture;\n        public pRENDERDOC_IsTargetControlConnected IsTargetControlConnected;\n        public pRENDERDOC_LaunchReplayUI LaunchReplayUI;\n        public pRENDERDOC_SetActiveWindow SetActiveWindow;\n        public pRENDERDOC_StartFrameCapture StartFrameCapture;\n        public pRENDERDOC_IsFrameCapturing IsFrameCapturing;\n        public pRENDERDOC_EndFrameCapture EndFrameCapture;\n        public pRENDERDOC_TriggerMultiFrameCapture TriggerMultiFrameCapture;\n        public pRENDERDOC_SetCaptureFileComments SetCaptureFileComments;\n        public pRENDERDOC_DiscardFrameCapture DiscardFrameCapture;\n        public pRENDERDOC_ShowReplayUI ShowReplayUI;\n\n        public RenderdocApi(Pointer data) {\n            super(data);\n            this.read();\n        }\n\n        public interface pRENDERDOC_GetAPIVersion extends Callback {\n            void call(IntByReference major, IntByReference minor, IntByReference patch);\n        }\n\n        public interface pRENDERDOC_SetCaptureOptionU32 extends Callback {\n            int call(int opt, uint32_t val);\n        }\n\n        public interface pRENDERDOC_SetCaptureOptionF32 extends Callback {\n            int call(int opt, float val);\n        }\n\n        public interface pRENDERDOC_GetCaptureOptionU32 extends Callback {\n            uint32_t call(int opt);\n        }\n\n        public interface pRENDERDOC_GetCaptureOptionF32 extends Callback {\n            float call(int opt);\n        }\n\n        public interface pRENDERDOC_SetFocusToggleKeys extends Callback {\n            void call(Pointer keys, int num);\n        }\n\n        public interface pRENDERDOC_SetCaptureKeys extends Callback {\n            void call(int[] keys, int num);\n        }\n\n        public interface pRENDERDOC_GetOverlayBits extends Callback {\n            uint32_t call();\n        }\n\n        public interface pRENDERDOC_MaskOverlayBits extends Callback {\n            void call(uint32_t And, uint32_t Or);\n        }\n\n        public interface pRENDERDOC_RemoveHooks extends Callback {\n            void call();\n        }\n\n        public interface pRENDERDOC_UnloadCrashHandler extends Callback {\n            void call();\n        }\n\n        public interface pRENDERDOC_SetCaptureFilePathTemplate extends Callback {\n            void call(String pathTemplate);\n        }\n\n        public interface pRENDERDOC_GetCaptureFilePathTemplate extends Callback {\n            String call();\n        }\n\n        public interface pRENDERDOC_GetNumCaptures extends Callback {\n            uint32_t call();\n        }\n\n        public interface pRENDERDOC_GetCapture extends Callback {\n            uint32_t call(int idx, byte[] filename, IntByReference pathLength, LongByReference timestamp);\n        }\n\n        public interface pRENDERDOC_TriggerCapture extends Callback {\n            void call();\n        }\n\n        public interface pRENDERDOC_IsTargetControlConnected extends Callback {\n            uint32_t call();\n        }\n\n        public interface pRENDERDOC_LaunchReplayUI extends Callback {\n            uint32_t call(uint32_t connectTargetControl, String cmdline);\n        }\n\n        public interface pRENDERDOC_ShowReplayUI extends Callback {\n            uint32_t call();\n        }\n\n        public interface pRENDERDOC_SetActiveWindow extends Callback {\n            void call(Pointer device, Pointer windowHandle);\n        }\n\n        public interface pRENDERDOC_StartFrameCapture extends Callback {\n            void call(Pointer device, Pointer windowHandle);\n        }\n\n        public interface pRENDERDOC_IsFrameCapturing extends Callback {\n            uint32_t call();\n        }\n\n        public interface pRENDERDOC_EndFrameCapture extends Callback {\n            void call(Pointer device, Pointer windowHandle);\n        }\n\n        public interface pRENDERDOC_DiscardFrameCapture extends Callback {\n            void call(Pointer device, Pointer windowHandle);\n        }\n\n        public interface pRENDERDOC_TriggerMultiFrameCapture extends Callback {\n            void call(uint32_t numFrames);\n        }\n\n        public interface pRENDERDOC_SetCaptureFileComments extends Callback {\n            void call(String filePath, String comments);\n        }\n    }\n\n    class uint32_t extends IntegerType {\n        public uint32_t() {\n            this(0);\n        }\n\n        public uint32_t(int value) {\n            super(4, value, true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/renderdoc/RenderdocScreen.java",
    "content": "package io.wispforest.owo.renderdoc;\n\nimport io.wispforest.owo.ops.TextOps;\nimport io.wispforest.owo.ui.base.BaseOwoScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.CheckboxComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.component.LabelComponent;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.util.CommandOpenedScreen;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.EnumSet;\n\npublic class RenderdocScreen extends BaseOwoScreen<FlowLayout> implements CommandOpenedScreen {\n\n    private int ticks = 0;\n    private boolean setCaptureKey = false;\n    private @Nullable RenderDoc.Key scheduledKey = null;\n\n    private ButtonComponent captureKeyButton = null;\n    private LabelComponent captureLabel = null;\n\n    @Override\n    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {\n        return OwoUIAdapter.create(this, UIContainers::verticalFlow);\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        rootComponent.surface(Surface.VANILLA_TRANSLUCENT);\n\n        var overlayState = RenderDoc.getOverlayOptions();\n        rootComponent.child(\n                        UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                                .child(UIComponents.label(Component.literal(\"RenderDoc Controls\")).shadow(true).margins(Insets.top(5).withBottom(10)))\n                                .child(\n                                        UIContainers.grid(Sizing.content(), Sizing.content(), 2, 2)\n                                                .child(overlayControl(Component.nullToEmpty(\"Enabled\"), overlayState, RenderDoc.OverlayOption.ENABLED), 0, 0)\n                                                .child(overlayControl(Component.nullToEmpty(\"Capture List\"), overlayState, RenderDoc.OverlayOption.CAPTURE_LIST), 0, 1)\n                                                .child(overlayControl(Component.nullToEmpty(\"Frame Rate\"), overlayState, RenderDoc.OverlayOption.FRAME_RATE), 1, 0)\n                                                .child(overlayControl(Component.nullToEmpty(\"Frame Number\"), overlayState, RenderDoc.OverlayOption.FRAME_NUMBER), 1, 1)\n                                )\n                                .child(\n                                        UIComponents.box(Sizing.fixed(175), Sizing.fixed(1))\n                                                .color(Color.ofFormatting(ChatFormatting.DARK_GRAY))\n                                                .fill(true)\n                                                .margins(Insets.vertical(5))\n                                )\n                                .child(\n                                        UIContainers.grid(Sizing.content(), Sizing.content(), 2, 2)\n                                                .child(UIComponents.button(\n                                                        Component.nullToEmpty(\"Launch UI\"),\n                                                        (ButtonComponent button) -> RenderDoc.launchReplayUI(true)\n                                                ).horizontalSizing(Sizing.fixed(90)).margins(Insets.of(2)), 0, 0)\n                                                .child((this.captureKeyButton = UIComponents.button(\n                                                        Component.nullToEmpty(\"Capture Hotkey\"),\n                                                        (ButtonComponent button) -> {\n                                                            button.active = false;\n                                                            button.setMessage(Component.nullToEmpty(\"Press...\"));\n\n                                                            this.setCaptureKey = true;\n                                                        }\n                                                )).horizontalSizing(Sizing.fixed(90)).margins(Insets.of(2)), 1, 0)\n                                                .child(UIComponents.button(\n                                                        Component.nullToEmpty(\"Capture Frame\"),\n                                                        (ButtonComponent button) -> RenderDoc.triggerCapture()\n                                                ).horizontalSizing(Sizing.fixed(90)).margins(Insets.of(2)), 0, 1)\n                                                .child(this.captureLabel = UIComponents.label(\n                                                        this.createCapturesText()\n                                                ), 1, 1)\n                                                .verticalAlignment(VerticalAlignment.CENTER).horizontalAlignment(HorizontalAlignment.CENTER)\n                                )\n                                .horizontalAlignment(HorizontalAlignment.CENTER)\n                                .padding(Insets.of(5))\n                                .surface(Surface.flat(0x77000000).and(Surface.outline(0x77000000)))\n                )\n                .verticalAlignment(VerticalAlignment.CENTER)\n                .horizontalAlignment(HorizontalAlignment.CENTER);\n    }\n\n    @Override\n    public void tick() {\n        super.tick();\n        if (++this.ticks % 10 != 0) return;\n\n        if (this.scheduledKey != null) {\n            RenderDoc.setCaptureKeys(this.scheduledKey);\n            this.scheduledKey = null;\n        }\n\n        this.captureLabel.text(this.createCapturesText());\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (this.setCaptureKey) {\n            this.captureKeyButton.active = true;\n            this.captureKeyButton.setMessage(Component.nullToEmpty(\"Capture Hotkey\"));\n\n            this.setCaptureKey = false;\n\n            var key = RenderDoc.Key.fromGLFW(input.key());\n            if (key != null) {\n                this.ticks = 0;\n                this.scheduledKey = key;\n\n                return true;\n            }\n        }\n        return super.keyPressed(input);\n    }\n\n    private Component createCapturesText() {\n        return TextOps.withColor(\"Captures: §\" + RenderDoc.getNumCaptures(), TextOps.color(ChatFormatting.WHITE), 0x00D7FF);\n    }\n\n    private static CheckboxComponent overlayControl(Component name, EnumSet<RenderDoc.OverlayOption> state, RenderDoc.OverlayOption option) {\n        var checkbox = UIComponents.checkbox(name);\n        checkbox.margins(Insets.of(3)).horizontalSizing(Sizing.fixed(100));\n        checkbox.checked(state.contains(option));\n        checkbox.onChanged(enabled -> {\n            if (enabled) {\n                RenderDoc.enableOverlayOptions(option);\n            } else {\n                RenderDoc.disableOverlayOptions(option);\n            }\n        });\n        return checkbox;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/CodecUtils.java",
    "content": "package io.wispforest.owo.serialization;\n\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonPrimitive;\nimport com.mojang.datafixers.util.Either;\nimport com.mojang.datafixers.util.Pair;\nimport com.mojang.serialization.*;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.wispforest.endec.*;\nimport io.wispforest.endec.format.bytebuf.ByteBufDeserializer;\nimport io.wispforest.endec.format.bytebuf.ByteBufSerializer;\nimport io.wispforest.endec.format.edm.*;\nimport io.wispforest.endec.format.forwarding.ForwardingDeserializer;\nimport io.wispforest.endec.format.forwarding.ForwardingSerializer;\nimport io.wispforest.endec.format.gson.GsonDeserializer;\nimport io.wispforest.endec.format.gson.GsonEndec;\nimport io.wispforest.endec.format.gson.GsonSerializer;\nimport io.wispforest.owo.mixin.serialization.DelegatingOpsAccessor;\nimport io.wispforest.owo.mixin.serialization.RegistryOpsAccessor;\nimport io.wispforest.owo.serialization.endec.EitherEndec;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.serialization.endec.StructEitherEndec;\nimport io.wispforest.owo.serialization.format.ContextHolder;\nimport io.wispforest.owo.serialization.format.DynamicOpsWithContext;\nimport io.wispforest.owo.serialization.format.edm.EdmOps;\nimport io.wispforest.owo.serialization.format.nbt.NbtDeserializer;\nimport io.wispforest.owo.serialization.format.nbt.NbtEndec;\nimport io.wispforest.owo.serialization.format.nbt.NbtSerializer;\nimport io.wispforest.owo.util.Scary;\nimport io.wispforest.owo.util.StackTraceSupplier;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.NbtOps;\nimport net.minecraft.nbt.StringTag;\nimport net.minecraft.nbt.Tag;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.resources.DelegatingOps;\nimport net.minecraft.resources.RegistryOps;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\n\npublic class CodecUtils {\n\n    /**\n     * Create a new endec serializing the same data as {@code codec}\n     * <p>\n     * This method is implemented by converting all data to be (de-)serialized\n     * to the Endec Data Model data format (hereto-forth to be referred to as EDM)\n     * which has both an endec ({@link EdmEndec}) and DynamicOps implementation ({@link EdmOps}).\n     * Since EDM encodes structure using a self-described format's native structural types,\n     * <b>this means that for JSON and NBT, the created endec's serialized representation is identical\n     * to that of {@code codec}</b>. In general, for non-self-described formats, the serialized\n     * representation is a byte array\n     * <p>\n     * When decoding, an EDM element is read from the deserializer and then parsed using {@code codec}\n     * <p>\n     * When encoding, the value is encoded using {@code codec} to an EDM element which is then\n     * written into the serializer\n     */\n    public static <T> Endec<T> toEndec(Codec<T> codec) {\n        return Endec.of(encoderOfCodec(codec), decoderOfCodec(codec));\n    }\n\n    private static <T> Endec.Encoder<T> encoderOfCodec(Codec<T> codec) {\n        return (ctx, serializer, value) -> encodeWithCodecIntoSerializer(codec, value, serializer, ctx);\n    }\n\n    private static <T, S> void encodeWithCodecIntoSerializer(Codec<T> codec, T value, Serializer<S> serializer, SerializationContext ctx) {\n        var unpackedSerializer = unpackSerializer(serializer);\n        var pair = getOpsAndAdapter(unpackedSerializer, ctx);\n\n        if (pair == null || !(unpackedSerializer instanceof SelfDescribedSerializer<S> selfDescribedSerializer)) {\n            EdmEndec.INSTANCE.encode(ctx, serializer, codec.encodeStart(createEdmOps(ctx), value).getOrThrow());\n        } else {\n            var ops = pair.getFirst();\n            var adapter = pair.getSecond();\n\n            encodeValue(adapter, selfDescribedSerializer, codec.encodeStart(ops, value).getOrThrow());\n        }\n    }\n\n    private static <T> Endec.Decoder<T> decoderOfCodec(Codec<T> codec) {\n        return (ctx, deserializer) -> decodeWithCodecFromDeserializer(codec, deserializer, ctx);\n    }\n\n    private static <T, S> T decodeWithCodecFromDeserializer(Codec<T> codec, Deserializer<S> deserializer, SerializationContext ctx) {\n        var unpackedDeserializer = unpackDeserializer(deserializer);\n        var pair = CodecUtils.getOpsAndAdapter(unpackedDeserializer, ctx);\n\n        return pair == null || !(unpackedDeserializer instanceof SelfDescribedDeserializer<S> selfDescribedDeserializer)\n            ? codec.parse(createEdmOps(ctx), EdmEndec.INSTANCE.decode(ctx, deserializer)).getOrThrow()\n            : codec.parse(pair.getFirst(), copyDecodedValue(pair.getSecond(), selfDescribedDeserializer)).getOrThrow();\n    }\n\n    public static <T> Endec<T> toEndec(Codec<T> codec, StreamCodec<ByteBuf, T> packetCodec) {\n        var encoder = encoderOfCodec(codec);\n        var decoder = decoderOfCodec(codec);\n\n        return Endec.of(\n            (ctx, serializer, value) -> {\n                if (serializer instanceof ByteBufSerializer<?>) {\n                    var buffer = new FriendlyByteBuf(Unpooled.buffer());\n                    packetCodec.encode(buffer, value);\n                    MinecraftEndecs.FRIENDLY_BYTE_BUF.encode(ctx, serializer, buffer);\n                } else {\n                    encoder.encode(ctx, serializer, value);\n                }\n            },\n            (ctx, deserializer) -> {\n                if (deserializer instanceof ByteBufDeserializer) {\n                    return packetCodec.decode(MinecraftEndecs.FRIENDLY_BYTE_BUF.decode(ctx, deserializer));\n                } else {\n                    return decoder.decode(ctx, deserializer);\n                }\n            }\n        );\n    }\n\n    public static <T> Endec<T> toEndecWithRegistries(Codec<T> codec, StreamCodec<RegistryFriendlyByteBuf, T> packetCodec) {\n        var encoder = encoderOfCodec(codec);\n        var decoder = decoderOfCodec(codec);\n\n        return Endec.of(\n            (ctx, serializer, value) -> {\n                if (serializer instanceof ByteBufSerializer<?>) {\n                    var buffer = new RegistryFriendlyByteBuf(new FriendlyByteBuf(Unpooled.buffer()), ctx.requireAttributeValue(RegistriesAttribute.REGISTRIES).registryAccess());\n\n                    packetCodec.encode(buffer, value);\n\n                    MinecraftEndecs.FRIENDLY_BYTE_BUF.encode(ctx, serializer, buffer);\n                } else {\n                    encoder.encode(ctx, serializer, value);\n                }\n            },\n            (ctx, deserializer) -> {\n                if (deserializer instanceof ByteBufDeserializer) {\n                    return packetCodec.decode(\n                        new RegistryFriendlyByteBuf(\n                            MinecraftEndecs.FRIENDLY_BYTE_BUF.decode(ctx, deserializer),\n                            ctx.requireAttributeValue(RegistriesAttribute.REGISTRIES).registryAccess()\n                        ));\n                } else {\n                    return decoder.decode(ctx, deserializer);\n                }\n            }\n        );\n    }\n\n    /**\n     * Create an endec which serializes an instance of {@link Either}, using {@code first}\n     * for the left and {@code second} for the right variant\n     * <p>\n     * In a self-describing format, the serialized representation is simply that of the endec of\n     * whichever variant is represented. In the general for non-self-described formats, the\n     * which variant is represented must also be stored\n     */\n    public static <F, S> Endec<Either<F, S>> eitherEndec(Endec<F> first, Endec<S> second) {\n        return new EitherEndec<>(first, second, false);\n    }\n\n    /**\n     * Like {@link #eitherEndec(Endec, Endec)}, but ensures when decoding from a self-described format\n     * that only {@code first} or {@code second}, but not both, succeed\n     */\n    public static <F, S> Endec<Either<F, S>> xorEndec(Endec<F> first, Endec<S> second) {\n        return new EitherEndec<>(first, second, true);\n    }\n\n    /**\n     * Create a structured endec which serializes an instance of {@link Either}, using {@code first}\n     * for the left and {@code second} for the right variant\n     * <p>\n     * In a self-describing format, the serialized representation is simply that of the endec of\n     * whichever variant is represented. In the general for non-self-described formats, the\n     * which variant is represented must also be stored\n     */\n    public static <F, S> StructEndec<Either<F, S>> eitherStructEndec(StructEndec<F> first, StructEndec<S> second) {\n        return new StructEitherEndec<>(first, second, false);\n    }\n\n    /**\n     * Like {@link #eitherStructEndec(StructEndec, StructEndec)}, but ensures when decoding from a self-described format\n     * that only {@code first} or {@code second}, but not both, succeed\n     */\n    public static <F, S> StructEndec<Either<F, S>> xorStructEndec(StructEndec<F> first, StructEndec<S> second) {\n        return new StructEitherEndec<>(first, second, true);\n    }\n\n    //--\n\n    /**\n     * Create a codec serializing the same data as this endec, assuming\n     * that the serialized format posses the {@code assumedAttributes}\n     * <p>\n     * This method is implemented by converting between a given DynamicOps'\n     * datatype and EDM (see {@link #toEndec(Codec)}) and then encoding/decoding\n     * from/to an EDM element using the {@link EdmSerializer} and {@link EdmDeserializer}\n     * <p>\n     * The serialized representation of a codec created through this method is generally\n     * identical to that of a codec manually created to describe the same data\n     */\n    public static <T> Codec<T> toCodec(Endec<T> endec, SerializationContext assumedContext) {\n        return new Codec<>() {\n            @Override\n            public <D> DataResult<Pair<T, D>> decode(DynamicOps<D> ops, D input) {\n                return captureThrows(() -> {\n                    var deserializer = deserializerForValue(ops, input);\n                    var context = createContext(ops, assumedContext);\n\n                    var decodedValue = (deserializer != null)\n                        ? endec.decode(deserializer.setupContext(context), deserializer)\n                        : endec.decode(context, LenientEdmDeserializer.of(ops.convertTo(EdmOps.withoutContext(), input)));\n\n                    return new Pair<>(decodedValue, input);\n                });\n            }\n\n            @Override\n            public <D> DataResult<D> encode(T input, DynamicOps<D> ops, D prefix) {\n                return captureThrows(() -> {\n                    var serializer = serializerForOps(ops);\n                    var context = createContext(ops, assumedContext);\n\n                    return (serializer != null)\n                        ? endec.encodeFully(context, () -> serializer, input)\n                        : EdmOps.withoutContext().convertTo(ops, endec.encodeFully(context, EdmSerializer::of, input));\n                });\n            }\n        };\n    }\n\n    public static <T> Codec<T> toCodec(Endec<T> endec) {\n        return toCodec(endec, SerializationContext.empty());\n    }\n\n    public static <T> MapCodec<T> toMapCodec(StructEndec<T> structEndec, SerializationContext assumedContext) {\n        return new MapCodec<>() {\n            @Override\n            public <T1> Stream<T1> keys(DynamicOps<T1> ops) {\n                throw new UnsupportedOperationException(\"MapCodec generated from StructEndec cannot report keys\");\n            }\n\n            @Override\n            public <T1> DataResult<T> decode(DynamicOps<T1> ops, MapLike<T1> input) {\n                return captureThrows(() -> {\n                    var deserializer = deserializerForMapLike(ops, input);\n                    var context = createContext(ops, assumedContext);\n\n                    if (deserializer != null) {\n                        return structEndec.decode(deserializer.setupContext(context), deserializer);\n                    } else {\n                        var map = new HashMap<String, EdmElement<?>>();\n\n                        input.entries().forEach(pair -> {\n                            map.put(\n                                ops.getStringValue(pair.getFirst()).getOrThrow(s -> new IllegalStateException(\"Unable to parse key: \" + s)),\n                                ops.convertTo(EdmOps.withoutContext(), pair.getSecond())\n                            );\n                        });\n\n                        return structEndec.decode(context, LenientEdmDeserializer.of(EdmElement.consumeMap(map)));\n                    }\n                });\n            }\n\n            @Override\n            public <T1> RecordBuilder<T1> encode(T input, DynamicOps<T1> ops, RecordBuilder<T1> prefix) {\n                try {\n                    var context = createContext(ops, assumedContext);\n                    var pair = serializerForRecordBuilder(ops, prefix);\n\n                    if (pair != null) {\n                        var serializer = pair.getFirst();\n                        return pair.getSecond().apply(structEndec.encodeFully(serializer.setupContext(context), () -> serializer, input));\n                    } else {\n                        var element = structEndec.encodeFully(context, EdmSerializer::of, input).<Map<String, EdmElement<?>>>cast();\n\n                        var result = prefix;\n                        for (var entry : element.entrySet()) {\n                            result = result.add(entry.getKey(), EdmOps.withoutContext().convertTo(ops, entry.getValue()));\n                        }\n\n                        return result;\n                    }\n                } catch (Exception e) {\n                    return prefix.withErrorsFrom(DataResult.error(e::getMessage, input));\n                }\n            }\n        };\n    }\n\n    public static <T> MapCodec<T> toMapCodec(StructEndec<T> structEndec) {\n        return toMapCodec(structEndec, SerializationContext.empty());\n    }\n\n    /*\n     * This method overall should be fine but do not expect such to work always as it could be a problem as\n     * it bypasses certain features about Deserializer API that may be an issue but is low chance for general\n     * cases within Minecraft.\n     *\n     * blodhgarm: 21.07.2024\n     */\n    @Scary\n    @ApiStatus.Experimental\n    public static <T> StructEndec<T> toStructEndec(MapCodec<T> mapCodec) {\n        return new StructEndec<T>() {\n            @Override\n            public void encodeStruct(SerializationContext ctx, Serializer<?> serializer, Serializer.Struct struct, T value) {\n                this.doStructEncode(ctx, serializer, struct, value);\n            }\n\n            private <S> void doStructEncode(SerializationContext ctx, Serializer<S> serializer, Serializer.Struct struct, T value) {\n                var unpackedSerializer = unpackSerializer(serializer);\n                var pair = getOpsAndAdapter(unpackedSerializer, ctx);\n\n                if (pair == null || !(unpackedSerializer instanceof SelfDescribedSerializer<S> selfDescribedSerializer)) {\n                    var edmOps = createEdmOps(ctx);\n\n                    var edmMap = mapCodec.encode(value, edmOps, edmOps.mapBuilder()).build(edmOps.emptyMap())\n                        .getOrThrow()\n                        .asMap();\n\n                    if (serializer instanceof SelfDescribedSerializer<?>) {\n                        edmMap.value().forEach((s, element) -> struct.field(s, ctx, EdmEndec.INSTANCE, element));\n                    } else {\n                        struct.field(\"element\", ctx, EdmEndec.MAP, edmMap);\n                    }\n                } else {\n                    CodecUtils.encodeStruct(pair.getSecond(), pair.getFirst(), selfDescribedSerializer, struct, mapCodec, value);\n                }\n            }\n\n            @Override\n            public T decodeStruct(SerializationContext ctx, Deserializer<?> deserializer, Deserializer.Struct struct) {\n                return this.doStructDecode(ctx, deserializer, struct);\n            }\n\n            private <S> T doStructDecode(SerializationContext ctx, Deserializer<S> deserializer, Deserializer.Struct struct) {\n                var unpackedDeserializer = unpackDeserializer(deserializer);\n                var pair = getOpsAndAdapter(unpackedDeserializer, ctx);\n\n                if (pair == null || !(unpackedDeserializer instanceof SelfDescribedDeserializer<S> selfDescribedDeserializer)) {\n                    var edmMap = ((deserializer instanceof SelfDescribedDeserializer<?>)\n                        ? EdmEndec.MAP.decode(ctx, deserializer)\n                        : struct.field(\"element\", ctx, EdmEndec.MAP));\n\n                    var ops = createEdmOps(ctx);\n                    return mapCodec.decode(ops, ops.getMap(edmMap).getOrThrow()).getOrThrow();\n                } else {\n                    return CodecUtils.decodeStruct(pair.getSecond(), pair.getFirst(), selfDescribedDeserializer, struct, mapCodec);\n                }\n            }\n        };\n    }\n\n    // the fact that we lose context here is certainly far from ideal,\n    // but for the most part *shouldn't* matter. after all, ideally nobody\n    // should ever be nesting packet codecs into endecs - there's little\n    // point to doing that and transferring any kind of context data becomes\n    // mostly impossible because the system turns into one opaque spaghetti mess\n    //\n    // glisco, 28.04.2024\n    public static <B extends FriendlyByteBuf, T> StreamCodec<B, T> toPacketCodec(Endec<T> endec) {\n        return new StreamCodec<>() {\n            @Override\n            public T decode(B buf) {\n                var ctx = buf instanceof RegistryFriendlyByteBuf registryByteBuf\n                    ? SerializationContext.attributes(RegistriesAttribute.of(registryByteBuf.registryAccess()))\n                    : SerializationContext.empty();\n\n                return endec.decode(ctx, ByteBufDeserializer.of(buf));\n            }\n\n            @Override\n            public void encode(B buf, T value) {\n                var ctx = buf instanceof RegistryFriendlyByteBuf registryByteBuf\n                    ? SerializationContext.attributes(RegistriesAttribute.of(registryByteBuf.registryAccess()))\n                    : SerializationContext.empty();\n\n                endec.encode(ctx, ByteBufSerializer.of(buf), value);\n            }\n        };\n    }\n\n    // ---\n\n    public static SerializationContext createContext(DynamicOps<?> ops, SerializationContext assumedContext) {\n        var rootOps = ops;\n        var context = rootOps instanceof ContextHolder holder\n            ? holder.capturedContext().and(assumedContext)\n            : null;\n\n        while (rootOps instanceof DelegatingOps<?>) {\n            rootOps = ((DelegatingOpsAccessor<?>) rootOps).owo$delegate();\n\n            if (context == null && rootOps instanceof ContextHolder holder) {\n                context = holder.capturedContext().and(assumedContext);\n            }\n        }\n\n        if (context == null) context = assumedContext;\n\n        if (ops instanceof RegistryOps<?> registryOps) {\n            context = context.withAttributes(RegistriesAttribute.tryFromCachedInfoGetter(((RegistryOpsAccessor) registryOps).owo$infoGetter()));\n        }\n\n        return context;\n    }\n\n    private static DynamicOps<EdmElement<?>> createEdmOps(SerializationContext ctx) {\n        DynamicOps<EdmElement<?>> ops = EdmOps.withContext(ctx);\n\n        if (ctx.hasAttribute(RegistriesAttribute.REGISTRIES)) {\n            ops = RegistryOps.create(ops, ctx.getAttributeValue(RegistriesAttribute.REGISTRIES).infoGetter());\n        }\n\n        return ops;\n    }\n\n    private static <T> DataResult<T> captureThrows(Supplier<T> action) {\n        try {\n            return DataResult.success(action.get());\n        } catch (Exception e) {\n            return DataResult.error(StackTraceSupplier.of(e));\n        }\n    }\n\n    // --- codec adapter shenanigans ensue ---\n\n    private static final Map<Class<? extends Serializer<?>>, CodecAdapter<?, ?, ?>> serializerToAdapter = new HashMap<>();\n    private static final Map<Class<? extends Deserializer<?>>, CodecAdapter<?, ?, ?>> deserializerToAdapter = new HashMap<>();\n    private static final Map<Class<? extends DynamicOps<?>>, CodecAdapter<?, ?, ?>> opsToAdapter = new HashMap<>();\n\n    @ApiStatus.Experimental\n    public static void registerCodecAdapter(CodecAdapter<?, ?, ?> adapter) {\n        if (serializerToAdapter.containsKey(adapter.serializerClass())) {\n            throw new IllegalStateException(\"Serializer class \" + adapter.serializerClass().getSimpleName() + \" is already managed by a different codec adapter\");\n        }\n        if (deserializerToAdapter.containsKey(adapter.deserializerClass())) {\n            throw new IllegalStateException(\"Deserializer class \" + adapter.deserializerClass().getSimpleName() + \" is already managed by a different codec adapter\");\n        }\n        if (opsToAdapter.containsKey(adapter.opsClass())) {\n            throw new IllegalStateException(\"DynamicOps class \" + adapter.opsClass().getSimpleName() + \" is already managed by a different codec adapter\");\n        }\n\n        serializerToAdapter.put(adapter.serializerClass(), adapter);\n        deserializerToAdapter.put(adapter.deserializerClass(), adapter);\n        opsToAdapter.put(adapter.opsClass(), adapter);\n    }\n\n    private static <T> DynamicOps<T> unpackOps(DynamicOps<T> ops) {\n        var rootOps = ops;\n        while (rootOps instanceof DelegatingOps<T>) rootOps = ((DelegatingOpsAccessor<T>) rootOps).owo$delegate();\n        return rootOps;\n    }\n\n    private static <T> Serializer<T> unpackSerializer(Serializer<T> serializer) {\n        var rootSerializer = serializer;\n        while (rootSerializer instanceof ForwardingSerializer<T> forwardingSerializer) rootSerializer = forwardingSerializer.delegate();\n        return rootSerializer;\n    }\n\n    private static <T> Deserializer<T> unpackDeserializer(Deserializer<T> deserializer) {\n        var rootDeserializer = deserializer;\n        while (rootDeserializer instanceof ForwardingDeserializer<T> forwardingDeserializer) rootDeserializer = forwardingDeserializer.delegate();\n        return rootDeserializer;\n    }\n\n    @Nullable\n    @SuppressWarnings(\"unchecked\")\n    private static <T, S extends SelfDescribedSerializer<T>> Pair<DynamicOps<T>, CodecAdapter<T, S, ?>> getOpsAndAdapter(Serializer<T> serializer, SerializationContext ctx) {\n        var adapter = (CodecAdapter<T, S, ?>) serializerToAdapter.get(serializer.getClass());\n        if (adapter == null) return null;\n\n        DynamicOps<T> ops = DynamicOpsWithContext.of(ctx, adapter.getOps());\n        if (ctx.hasAttribute(RegistriesAttribute.REGISTRIES)) {\n            ops = RegistryOps.create(ops, ctx.getAttributeValue(RegistriesAttribute.REGISTRIES).infoGetter());\n        }\n\n        return new Pair<>(ops, adapter);\n    }\n\n    @Nullable\n    @SuppressWarnings(\"unchecked\")\n    private static <T, D extends SelfDescribedDeserializer<T>> Pair<DynamicOps<T>, CodecAdapter<T, ?, D>> getOpsAndAdapter(Deserializer<T> deserializer, SerializationContext ctx) {\n        var adapter = (CodecAdapter<T, ?, D>) deserializerToAdapter.get(deserializer.getClass());\n        if (adapter == null) return null;\n\n        DynamicOps<T> ops = DynamicOpsWithContext.of(ctx, adapter.getOps());\n        if (ctx.hasAttribute(RegistriesAttribute.REGISTRIES)) {\n            ops = RegistryOps.create(ops, ctx.getAttributeValue(RegistriesAttribute.REGISTRIES).infoGetter());\n        }\n\n        return new Pair<>(ops, adapter);\n    }\n\n    @Nullable\n    @SuppressWarnings({\"unchecked\", \"SuspiciousMethodCalls\"})\n    private static <T> Serializer<T> serializerForOps(DynamicOps<T> dynamicOps) {\n        var adapter = (CodecAdapter<T, SelfDescribedSerializer<T>, ?>) opsToAdapter.get(unpackOps(dynamicOps).getClass());\n        return adapter != null ? adapter.createSerializer() : null;\n    }\n\n    @Nullable\n    @SuppressWarnings({\"unchecked\", \"SuspiciousMethodCalls\"})\n    private static <T> Deserializer<T> deserializerForValue(DynamicOps<T> dynamicOps, T value) {\n        var adapter = (CodecAdapter<T, ?, SelfDescribedDeserializer<T>>) opsToAdapter.get(unpackOps(dynamicOps).getClass());\n        return (adapter != null) ? adapter.createDeserializer(value) : null;\n    }\n\n    @Nullable\n    @SuppressWarnings({\"unchecked\", \"SuspiciousMethodCalls\"})\n    private static <T> Pair<Serializer<T>, Function<T, RecordBuilder<T>>> serializerForRecordBuilder(DynamicOps<T> dynamicOps, RecordBuilder<T> builder) {\n        var adapter = (CodecAdapter<T, SelfDescribedSerializer<T>, ?>) opsToAdapter.get(unpackOps(dynamicOps).getClass());\n\n        return (adapter != null)\n            ? new Pair<>(adapter.createSerializer(), t -> adapter.addToBuilder(t, builder))\n            : null;\n    }\n\n    @Nullable\n    @SuppressWarnings({\"unchecked\", \"SuspiciousMethodCalls\"})\n    private static <T> Deserializer<T> deserializerForMapLike(DynamicOps<T> dynamicOps, MapLike<T> mapLike) {\n        var adapter = (CodecAdapter<T, ?, SelfDescribedDeserializer<T>>) opsToAdapter.get(unpackOps(dynamicOps).getClass());\n\n        return (adapter != null)\n            ? adapter.createDeserializer(adapter.unpackMapLike(mapLike))\n            : null;\n    }\n\n    //--\n\n    private static <T, S extends SelfDescribedSerializer<T>> void encodeValue(CodecAdapter<T, S, ?> adapter, S serializer, T value) {\n        adapter.createDeserializer(value).readAny(SerializationContext.empty(), serializer);\n    }\n\n    private static <T, D extends SelfDescribedDeserializer<T>> T copyDecodedValue(CodecAdapter<T, ?, D> adapter, D deserializer) {\n        var serializer = adapter.createSerializer();\n        deserializer.readAny(SerializationContext.empty(), serializer);\n        return serializer.result();\n    }\n\n    private static <T, V, S extends SelfDescribedSerializer<T>> void encodeStruct(CodecAdapter<T, S, ?> adapter, DynamicOps<T> ops, S serializer, Serializer.Struct struct, MapCodec<V> mapCodec, V value) {\n        var formatValue = mapCodec.encode(value, ops, ops.mapBuilder()).build(ops.emptyMap()).getOrThrow();\n        adapter.encodeStruct(SerializationContext.empty(), serializer, struct, formatValue);\n    }\n\n    private static <T, V, D extends SelfDescribedDeserializer<T>> V decodeStruct(CodecAdapter<T, ?, D> adapter, DynamicOps<T> ops, D deserializer, Deserializer.Struct struct, MapCodec<V> mapCodec) {\n        var formatValue = adapter.copyDecodedStruct(SerializationContext.empty(), deserializer, struct);\n        return mapCodec.decode(ops, ops.getMap(formatValue).getOrThrow()).getOrThrow();\n    }\n\n    public interface CodecAdapter<T, S extends SelfDescribedSerializer<T>, D extends SelfDescribedDeserializer<T>> {\n        Class<? extends Serializer<T>> serializerClass();\n        Class<? extends Deserializer<T>> deserializerClass();\n        Class<? extends DynamicOps<T>> opsClass();\n\n        // ---\n\n        S createSerializer();\n        D createDeserializer(T value);\n        DynamicOps<T> getOps();\n\n        // ---\n\n        T unpackMapLike(MapLike<T> mapLike);\n        RecordBuilder<T> addToBuilder(T value, RecordBuilder<T> builder);\n\n        // ---\n\n        void encodeStruct(SerializationContext ctx, S serializer, Serializer.Struct struct, T value);\n        T copyDecodedStruct(SerializationContext ctx, D serializer, Deserializer.Struct struct);\n    }\n\n    static {\n        registerCodecAdapter(new CodecAdapter<Tag, NbtSerializer, NbtDeserializer>() {\n            @Override\n            public Class<? extends Serializer<Tag>> serializerClass() {\n                return NbtSerializer.class;\n            }\n\n            @Override\n            public Class<? extends Deserializer<Tag>> deserializerClass() {\n                return NbtDeserializer.class;\n            }\n\n            @Override\n            public Class<? extends DynamicOps<Tag>> opsClass() {\n                return NbtOps.class;\n            }\n\n            @Override\n            public NbtSerializer createSerializer() {\n                return NbtSerializer.of();\n            }\n\n            @Override\n            public NbtDeserializer createDeserializer(Tag value) {\n                return NbtDeserializer.of(value);\n            }\n\n            @Override\n            public DynamicOps<Tag> getOps() {\n                return NbtOps.INSTANCE;\n            }\n\n            @Override\n            public Tag unpackMapLike(MapLike<Tag> mapLike) {\n                var compound = new CompoundTag();\n\n                mapLike.entries().forEach(pairs -> {\n                    var key = pairs.getFirst();\n                    var value = pairs.getSecond();\n\n                    if (!(key instanceof StringTag primitive)) {\n                        throw new IllegalStateException(\"Unable to parse key: \" + key);\n                    }\n\n                    compound.put(primitive.asString().get(), value);\n                });\n\n                return compound;\n            }\n\n            @Override\n            public RecordBuilder<Tag> addToBuilder(Tag value, RecordBuilder<Tag> builder) {\n                if (!(value instanceof CompoundTag compoundTag)) {\n                    throw new IllegalStateException(\"Cannot add non-NbtCompound value into record builder: \" + value);\n                }\n\n                var result = builder;\n                for (var key : compoundTag.keySet()) {\n                    result = result.add(key, compoundTag.get(key));\n                }\n\n                return result;\n            }\n\n            @Override\n            public void encodeStruct(SerializationContext ctx, NbtSerializer serializer, Serializer.Struct struct, Tag value) {\n                if (!(value instanceof CompoundTag compoundTag)) {\n                    throw new IllegalStateException(\"Cannot encode non-NbtCompound value as struct: \" + value);\n                }\n\n                compoundTag.keySet().forEach(key -> struct.field(key, ctx, NbtEndec.ELEMENT, compoundTag.get(key)));\n            }\n\n            @Override\n            public Tag copyDecodedStruct(SerializationContext ctx, NbtDeserializer deserializer, Deserializer.Struct struct) {\n                return NbtEndec.COMPOUND.decode(ctx, deserializer);\n            }\n        });\n\n        registerCodecAdapter(new CodecAdapter<JsonElement, GsonSerializer, GsonDeserializer>() {\n            @Override\n            public Class<? extends Serializer<JsonElement>> serializerClass() {\n                return GsonSerializer.class;\n            }\n\n            @Override\n            public Class<? extends Deserializer<JsonElement>> deserializerClass() {\n                return GsonDeserializer.class;\n            }\n\n            @Override\n            public Class<? extends DynamicOps<JsonElement>> opsClass() {\n                return JsonOps.class;\n            }\n\n            @Override\n            public GsonSerializer createSerializer() {\n                return GsonSerializer.of();\n            }\n\n            @Override\n            public GsonDeserializer createDeserializer(JsonElement value) {\n                return GsonDeserializer.of(value);\n            }\n\n            @Override\n            public DynamicOps<JsonElement> getOps() {\n                return JsonOps.INSTANCE;\n            }\n\n            @Override\n            public JsonElement unpackMapLike(MapLike<JsonElement> mapLike) {\n                var jsonObject = new JsonObject();\n\n                mapLike.entries().forEach(pairs -> {\n                    var key = pairs.getFirst();\n                    var value = pairs.getSecond();\n\n                    if (!(key instanceof JsonPrimitive primitive && primitive.isString())) {\n                        throw new IllegalStateException(\"Unable to parse key: \" + key);\n                    }\n\n                    jsonObject.add(primitive.getAsString(), value);\n                });\n\n                return jsonObject;\n            }\n\n            @Override\n            public RecordBuilder<JsonElement> addToBuilder(JsonElement value, RecordBuilder<JsonElement> builder) {\n                if (!(value instanceof JsonObject jsonObject)) {\n                    throw new IllegalStateException(\"Cannot add non-JsonObject value into record builder: \" + value);\n                }\n\n                var result = builder;\n                for (var entry : jsonObject.asMap().entrySet()) {\n                    result = result.add(entry.getKey(), entry.getValue());\n                }\n\n                return result;\n            }\n\n            @Override\n            public void encodeStruct(SerializationContext ctx, GsonSerializer serializer, Serializer.Struct struct, JsonElement value) {\n                if (!(value instanceof JsonObject jsonObject)) {\n                    throw new IllegalStateException(\"Cannot encode non-JsonObject value as struct: \" + value);\n                }\n\n                jsonObject.asMap().forEach((key, element) -> struct.field(key, ctx, GsonEndec.INSTANCE, element));\n            }\n\n            @Override\n            public JsonElement copyDecodedStruct(SerializationContext ctx, GsonDeserializer serializer, Deserializer.Struct struct) {\n                return GsonEndec.INSTANCE.decode(ctx, serializer);\n            }\n        });\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/EndecRecipeSerializer.java",
    "content": "package io.wispforest.owo.serialization;\n\nimport com.mojang.serialization.MapCodec;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationAttributes;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.StructEndec;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.network.codec.StreamCodec;\nimport net.minecraft.world.item.crafting.Recipe;\nimport net.minecraft.world.item.crafting.RecipeSerializer;\n\npublic class EndecRecipeSerializer<R extends Recipe<?>> implements RecipeSerializer<R> {\n\n    private final StreamCodec<FriendlyByteBuf, R> packetCodec;\n    private final MapCodec<R> codec;\n\n    public EndecRecipeSerializer(StructEndec<R> endec, Endec<R> networkEndec) {\n        this.packetCodec = CodecUtils.toPacketCodec(networkEndec);\n        this.codec = CodecUtils.toMapCodec(endec, SerializationContext.attributes(SerializationAttributes.HUMAN_READABLE));\n    }\n\n    public EndecRecipeSerializer(StructEndec<R> endec) {\n        this(endec, endec);\n    }\n\n    @Override\n    public MapCodec<R> codec() {\n        return this.codec;\n    }\n\n    @Override\n    public StreamCodec<RegistryFriendlyByteBuf, R> streamCodec() {\n        return this.packetCodec.cast();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/OwoDataComponentTypeBuilder.java",
    "content": "package io.wispforest.owo.serialization;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationContext;\nimport net.minecraft.core.component.DataComponentType;\n\npublic interface OwoDataComponentTypeBuilder<T> {\n    default DataComponentType.Builder<T> endec(Endec<T> endec) {\n        return this.endec(endec, SerializationContext.empty());\n    }\n\n    default DataComponentType.Builder<T> endec(Endec<T> endec, SerializationContext assumedContext) {\n        return ((DataComponentType.Builder<T>) this)\n            .persistent(CodecUtils.toCodec(endec, assumedContext))\n            .networkSynchronized(CodecUtils.toPacketCodec(endec));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/RegistriesAttribute.java",
    "content": "package io.wispforest.owo.serialization;\n\nimport io.wispforest.endec.SerializationAttribute;\nimport io.wispforest.owo.mixin.serialization.CachedRegistryInfoGetterAccessor;\nimport net.minecraft.core.RegistryAccess;\nimport net.minecraft.resources.RegistryOps;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\npublic final class RegistriesAttribute implements SerializationAttribute.Instance {\n\n    public static final SerializationAttribute.WithValue<RegistriesAttribute> REGISTRIES = SerializationAttribute.withValue(\"registries\");\n\n    private final RegistryOps.RegistryInfoLookup infoLookup;\n    private final @Nullable RegistryAccess registryAccess;\n\n    private RegistriesAttribute(RegistryOps.RegistryInfoLookup infoLookup, @Nullable RegistryAccess registryAccess) {\n        this.infoLookup = infoLookup;\n        this.registryAccess = registryAccess;\n    }\n\n    public static RegistriesAttribute of(RegistryAccess registryAccess) {\n        return new RegistriesAttribute(\n                new RegistryOps.HolderLookupAdapter(registryAccess),\n                registryAccess\n        );\n    }\n\n    @ApiStatus.Internal\n    public static RegistriesAttribute tryFromCachedInfoGetter(RegistryOps.RegistryInfoLookup lookup) {\n        return (lookup instanceof RegistryOps.HolderLookupAdapter cachedGetter)\n                ? fromCachedInfoGetter(cachedGetter)\n                : fromInfoGetter(lookup);\n    }\n\n    public static RegistriesAttribute fromCachedInfoGetter(RegistryOps.HolderLookupAdapter cachedGetter) {\n        RegistryAccess registryAccess = null;\n\n        if(((CachedRegistryInfoGetterAccessor) (Object) cachedGetter).owo$getRegistriesLookup() instanceof RegistryAccess drm) {\n            registryAccess = drm;\n        }\n\n        return new RegistriesAttribute(cachedGetter, registryAccess);\n    }\n\n    public static RegistriesAttribute fromInfoGetter(RegistryOps.RegistryInfoLookup lookup) {\n        return new RegistriesAttribute(lookup, null);\n    }\n\n    public RegistryOps.RegistryInfoLookup infoGetter() {\n        return this.infoLookup;\n    }\n\n    public boolean hasRegistryAccess() {\n        return this.registryAccess != null;\n    }\n\n    public @NotNull RegistryAccess registryAccess() {\n        if (!this.hasRegistryAccess()) {\n            throw new IllegalStateException(\"This instance of RegistriesAttribute does not supply RegistryAccess\");\n        }\n\n        return this.registryAccess;\n    }\n\n    @Override\n    public SerializationAttribute attribute() {\n        return REGISTRIES;\n    }\n\n    @Override\n    public Object value() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/endec/EitherEndec.java",
    "content": "package io.wispforest.owo.serialization.endec;\n\nimport com.mojang.datafixers.util.Either;\nimport io.wispforest.endec.*;\n\npublic final class EitherEndec<L, R> implements Endec<Either<L, R>> {\n\n    private final Endec<L> leftEndec;\n    private final Endec<R> rightEndec;\n\n    private final boolean exclusive;\n\n    public EitherEndec(Endec<L> leftEndec, Endec<R> rightEndec, boolean exclusive) {\n        this.leftEndec = leftEndec;\n        this.rightEndec = rightEndec;\n\n        this.exclusive = exclusive;\n    }\n\n    @Override\n    public void encode(SerializationContext ctx, Serializer<?> serializer, Either<L, R> either) {\n        if (serializer instanceof SelfDescribedSerializer<?>) {\n            either.ifLeft(left -> this.leftEndec.encode(ctx, serializer, left))\n                    .ifRight(right -> this.rightEndec.encode(ctx, serializer, right));\n        } else {\n            try (var struct = serializer.struct()) {\n                struct.field(\"is_left\", ctx, Endec.BOOLEAN, either.left().isPresent());\n\n                either.ifLeft(left -> struct.field(\"left\", ctx, this.leftEndec, left))\n                        .ifRight(right -> struct.field(\"right\", ctx, this.rightEndec, right));\n            }\n        }\n    }\n\n    @Override\n    public Either<L, R> decode(SerializationContext ctx, Deserializer<?> deserializer) {\n        boolean selfDescribing = deserializer instanceof SelfDescribedDeserializer<?>;\n\n        if (selfDescribing) {\n            Either<L, R> leftResult = null;\n            try {\n                leftResult = Either.left(deserializer.tryRead(deserializer1 -> this.leftEndec.decode(ctx, deserializer1)));\n            } catch (Exception ignore) {}\n\n            if (!this.exclusive && leftResult != null) return leftResult;\n\n            Either<L, R> rightResult = null;\n            try {\n                rightResult = Either.right(deserializer.tryRead(deserializer1 -> this.rightEndec.decode(ctx, deserializer1)));\n            } catch (Exception ignore) {}\n\n            if (this.exclusive && leftResult != null && rightResult != null) {\n                throw new IllegalStateException(\"Both alternatives read successfully, can not pick the correct one; first: \" + leftResult + \" second: \" + rightResult);\n            }\n\n            if (leftResult != null) return leftResult;\n            if (rightResult != null) return rightResult;\n\n            throw new IllegalStateException(\"Neither alternative read successfully\");\n        } else {\n            var struct = deserializer.struct(ctx);\n\n            return (struct.field(\"is_left\", ctx, Endec.BOOLEAN))\n                    ? Either.left(struct.field(\"left\", ctx, this.leftEndec))\n                    : Either.right(struct.field(\"right\", ctx, this.rightEndec));\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/endec/KeyedEndecDecodeError.java",
    "content": "package io.wispforest.owo.serialization.endec;\n\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.owo.Owo;\nimport net.minecraft.nbt.Tag;\nimport net.minecraft.util.ProblemReporter;\n\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\n\n// TODO: GET ENDEC TRACE WHEN USING LATEST ENDEC OR SOMETHING?\npublic record KeyedEndecDecodeError(KeyedEndec<?> key, Tag element, Exception exception, boolean sendEntireException) implements ProblemReporter.Problem {\n\n    public KeyedEndecDecodeError(KeyedEndec<?> key, Tag element, Exception exception) {\n        this(key, element, exception, Owo.DEBUG);\n    }\n\n    @Override\n    public String description() {\n        var message = new StringWriter();\n\n        var writer = new PrintWriter(message);\n\n        writer.println(\"Failed to decode value '\" + this.element + \"' from KeyedEndec '\" + this.key.key() + \"': \");\n\n        if (sendEntireException) {\n            writer.println(exception.getMessage());\n        } else {\n            exception.printStackTrace(writer);\n        }\n\n        return message.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/endec/KeyedEndecEncodeError.java",
    "content": "package io.wispforest.owo.serialization.endec;\n\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.owo.Owo;\nimport net.minecraft.util.ProblemReporter;\n\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\n\npublic record KeyedEndecEncodeError(KeyedEndec<?> key, Object obj, Exception exception, boolean encodedDefaultValue, boolean sendEntireException) implements ProblemReporter.Problem {\n\n    public KeyedEndecEncodeError(KeyedEndec<?> key, Object obj, Exception exception, boolean encodedDefaultValue) {\n        this(key, obj, exception, encodedDefaultValue, Owo.DEBUG);\n    }\n\n    @Override\n    public String description() {\n        var message = new StringWriter();\n\n        var writer = new PrintWriter(message);\n\n        writer.println(\"Failed to encode value '\" + this.obj + \"' with KeyedEndec '\" + this.key + \"'\" + (this.encodedDefaultValue ? \"and used default value instead\" : \"\") + \": \");\n\n        if (sendEntireException) {\n            writer.println(exception.getMessage());\n        } else {\n            exception.printStackTrace(writer);\n        }\n\n        return message.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/endec/MinecraftEndecs.java",
    "content": "package io.wispforest.owo.serialization.endec;\n\nimport com.mojang.datafixers.util.Function3;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationAttributes;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport net.fabricmc.fabric.api.networking.v1.PacketByteBufs;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.Vec3i;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.ComponentSerialization;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.ChunkPos;\nimport net.minecraft.world.phys.BlockHitResult;\nimport net.minecraft.world.phys.HitResult;\nimport net.minecraft.world.phys.Vec3;\nimport org.joml.Vector3f;\n\nimport java.util.List;\nimport java.util.function.Function;\n\npublic final class MinecraftEndecs {\n\n    private MinecraftEndecs() {}\n\n    // --- MC Types ---\n\n    public static final Endec<FriendlyByteBuf> FRIENDLY_BYTE_BUF = Endec.BYTES\n            .xmap(bytes -> {\n                var buffer = PacketByteBufs.create();\n                buffer.writeBytes(bytes);\n\n                return buffer;\n            }, buffer -> {\n                var rinx = buffer.readerIndex();\n\n                var bytes = new byte[buffer.readableBytes()];\n                buffer.readBytes(bytes);\n\n                buffer.readerIndex(rinx);\n\n                return bytes;\n            });\n\n    public static final Endec<Identifier> IDENTIFIER = Endec.STRING.xmap(Identifier::parse, Identifier::toString);\n    public static final Endec<ItemStack> ITEM_STACK = CodecUtils.toEndecWithRegistries(ItemStack.OPTIONAL_CODEC, ItemStack.OPTIONAL_STREAM_CODEC);\n    public static final Endec<Component> TEXT = CodecUtils.toEndec(ComponentSerialization.CODEC, ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC);\n\n    public static final Endec<Vec3i> VEC3I = vectorEndec(\"Vec3i\", Endec.INT, Vec3i::new, Vec3i::getX, Vec3i::getY, Vec3i::getZ);\n    public static final Endec<Vec3> VEC3 = vectorEndec(\"Vec3\", Endec.DOUBLE, Vec3::new, Vec3::x, Vec3::y, Vec3::z);\n    public static final Endec<Vector3f> VECTOR3F = vectorEndec(\"Vector3f\", Endec.FLOAT, Vector3f::new, Vector3f::x, Vector3f::y, Vector3f::z);\n\n    public static final Endec<BlockPos> BLOCK_POS = Endec\n            .ifAttr(\n                    SerializationAttributes.HUMAN_READABLE,\n                    vectorEndec(\"BlockPos\", Endec.INT, BlockPos::new, BlockPos::getX, BlockPos::getY, BlockPos::getZ)\n            ).orElse(\n                    Endec.LONG.xmap(BlockPos::of, BlockPos::asLong)\n            );\n\n    public static final Endec<ChunkPos> CHUNK_POS = Endec\n            .ifAttr(\n                    SerializationAttributes.HUMAN_READABLE,\n                    Endec.INT.listOf().validate(ints -> {\n                        if (ints.size() != 2) {\n                            throw new IllegalStateException(\"ChunkPos array must have two elements\");\n                        }\n                    }).xmap(\n                            ints -> new ChunkPos(ints.get(0), ints.get(1)),\n                            chunkPos -> List.of(chunkPos.x, chunkPos.z)\n                    )\n            )\n            .orElse(Endec.LONG.xmap(ChunkPos::new, ChunkPos::toLong));\n\n    public static final Endec<BlockHitResult> BLOCK_HIT_RESULT = StructEndecBuilder.of(\n            VEC3.fieldOf(\"pos\", BlockHitResult::getLocation),\n            Endec.forEnum(Direction.class).fieldOf(\"side\", BlockHitResult::getDirection),\n            BLOCK_POS.fieldOf(\"block_pos\", BlockHitResult::getBlockPos),\n            Endec.BOOLEAN.fieldOf(\"inside_block\", BlockHitResult::isInside),\n            Endec.BOOLEAN.fieldOf(\"missed\", $ -> $.getType() == HitResult.Type.MISS),\n            (pos, side, blockPos, insideBlock, missed) -> !missed\n                    ? new BlockHitResult(pos, side, blockPos, insideBlock)\n                    : BlockHitResult.miss(pos, side, blockPos)\n    );\n\n    // --- Constructors for MC types ---\n\n    public static ReflectiveEndecBuilder addDefaults(ReflectiveEndecBuilder builder) {\n        builder.register(FRIENDLY_BYTE_BUF, FriendlyByteBuf.class);\n\n        builder.register(IDENTIFIER, Identifier.class)\n                .register(ITEM_STACK, ItemStack.class)\n                .register(TEXT, Component.class);\n\n        builder.register(VEC3I, Vec3i.class)\n                .register(VEC3, Vec3.class)\n                .register(VECTOR3F, Vector3f.class);\n\n        builder.register(BLOCK_POS, BlockPos.class)\n                .register(CHUNK_POS, ChunkPos.class);\n\n        builder.register(BLOCK_HIT_RESULT, BlockHitResult.class);\n\n        return builder;\n    }\n\n    public static <T> Endec<T> ofRegistry(Registry<T> registry) {\n        return IDENTIFIER.xmap(registry::getValue, registry::getKey);\n    }\n\n    public static <T> Endec<TagKey<T>> unprefixedTagKey(ResourceKey<? extends Registry<T>> registry) {\n        return IDENTIFIER.xmap(id -> TagKey.create(registry, id), TagKey::location);\n    }\n\n    public static <T> Endec<TagKey<T>> prefixedTagKey(ResourceKey<? extends Registry<T>> registry) {\n        return Endec.STRING.xmap(\n                s -> TagKey.create(registry, Identifier.parse(s.substring(1))),\n                tag -> \"#\" + tag.location()\n        );\n    }\n\n    private static <C, V> Endec<V> vectorEndec(String name, Endec<C> componentEndec, Function3<C, C, C, V> constructor, Function<V, C> xGetter, Function<V, C> yGetter, Function<V, C> zGetter) {\n        return componentEndec.listOf().validate(ints -> {\n            if (ints.size() != 3) {\n                throw new IllegalStateException(name + \" array must have three elements\");\n            }\n        }).xmap(\n                components -> constructor.apply(components.get(0), components.get(1), components.get(2)),\n                vector -> List.of(xGetter.apply(vector), yGetter.apply(vector), zGetter.apply(vector))\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/endec/NonNullListEndec.java",
    "content": "package io.wispforest.owo.serialization.endec;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport net.minecraft.core.NonNullList;\n\nimport java.util.ArrayList;\nimport java.util.Objects;\nimport java.util.function.Predicate;\n\npublic final class NonNullListEndec {\n\n    private NonNullListEndec() {}\n\n    public static <T> Endec<NonNullList<T>> forSize(Endec<T> elementEndec, T defaultValue, int size) {\n        return forSize(elementEndec, defaultValue, element -> Objects.equals(defaultValue, element), size);\n    }\n\n    public static <T> Endec<NonNullList<T>> forSize(Endec<T> elementEndec, T defaultValue, Predicate<T> skipWhen, int size) {\n        var entryEndec = StructEndecBuilder.of(\n                elementEndec.fieldOf(\"element\", s -> s.element),\n                Endec.VAR_INT.fieldOf(\"idx\", s -> s.idx),\n                Entry::new\n        );\n\n        return entryEndec.listOf().xmap(\n                entries -> {\n                    var list = NonNullList.withSize(size, defaultValue);\n                    entries.forEach(entry -> list.set(entry.idx, entry.element));\n                    return list;\n                }, elements -> {\n                    var entries = new ArrayList<Entry<T>>();\n                    for (int i = 0; i < elements.size(); i++) {\n                        if (skipWhen.test(elements.get(i))) continue;\n                        entries.add(new Entry<>(elements.get(i), i));\n                    }\n                    return entries;\n                }\n        );\n    }\n\n    private record Entry<T>(T element, int idx) {}\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/endec/StructEitherEndec.java",
    "content": "package io.wispforest.owo.serialization.endec;\n\nimport com.mojang.datafixers.util.Either;\nimport io.wispforest.endec.*;\n\npublic final class StructEitherEndec<L, R> implements StructEndec<Either<L, R>> {\n\n    private final StructEndec<L> leftEndec;\n    private final StructEndec<R> rightEndec;\n\n    private final boolean exclusive;\n\n    public StructEitherEndec(StructEndec<L> leftEndec, StructEndec<R> rightEndec, boolean exclusive) {\n        this.leftEndec = leftEndec;\n        this.rightEndec = rightEndec;\n\n        this.exclusive = exclusive;\n    }\n\n    @Override\n    public void encodeStruct(SerializationContext ctx, Serializer<?> serializer, Serializer.Struct struct, Either<L, R> either) {\n        if (serializer instanceof SelfDescribedSerializer<?>) {\n            either.ifLeft(left -> this.leftEndec.encodeStruct(ctx, serializer, struct, left))\n                    .ifRight(right -> this.rightEndec.encodeStruct(ctx, serializer, struct, right));\n        } else {\n            struct.field(\"is_left\", ctx, Endec.BOOLEAN, either.left().isPresent());\n\n            either.ifLeft(left -> this.leftEndec.encodeStruct(ctx, serializer, struct, left))\n                    .ifRight(right -> this.rightEndec.encodeStruct(ctx, serializer, struct, right));\n        }\n    }\n\n    @Override\n    public Either<L, R> decodeStruct(SerializationContext ctx, Deserializer<?> deserializer, Deserializer.Struct struct) {\n        boolean selfDescribing = deserializer instanceof SelfDescribedDeserializer<?>;\n\n        if (selfDescribing) {\n            Either<L, R> leftResult = null;\n            try {\n                leftResult = Either.left(deserializer.tryRead(deserializer1 -> this.leftEndec.decodeStruct(ctx, deserializer1, struct)));\n            } catch (Exception ignore) {}\n\n            if (!this.exclusive && leftResult != null) return leftResult;\n\n            Either<L, R> rightResult = null;\n            try {\n                rightResult = Either.right(deserializer.tryRead(deserializer1 -> this.rightEndec.decodeStruct(ctx, deserializer1, struct)));\n            } catch (Exception ignore) {}\n\n            if (this.exclusive && leftResult != null && rightResult != null) {\n                throw new IllegalStateException(\"Both alternatives read successfully, can not pick the correct one; first: \" + leftResult + \" second: \" + rightResult);\n            }\n\n            if (leftResult != null) return leftResult;\n            if (rightResult != null) return rightResult;\n\n            throw new IllegalStateException(\"Neither alternative read successfully\");\n        } else {\n            var isLeft = struct.field(\"is_left\", ctx, Endec.BOOLEAN);\n\n            return isLeft ? Either.left(this.leftEndec.decodeStruct(ctx, deserializer, struct))\n                    : Either.right(this.rightEndec.decodeStruct(ctx, deserializer, struct));\n\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/format/ContextHolder.java",
    "content": "package io.wispforest.owo.serialization.format;\n\nimport io.wispforest.endec.SerializationContext;\n\n/**\n * A common interface for parts of a serialization infrastructure\n * which provide an instance of {@link SerializationContext}. Primarily\n * used for attaching context to {@link com.mojang.serialization.DynamicOps}\n */\npublic interface ContextHolder {\n    SerializationContext capturedContext();\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/format/DynamicOpsWithContext.java",
    "content": "package io.wispforest.owo.serialization.format;\n\nimport com.mojang.serialization.DynamicOps;\nimport io.wispforest.endec.SerializationContext;\nimport net.minecraft.resources.DelegatingOps;\n\npublic class DynamicOpsWithContext<T> extends DelegatingOps<T> implements ContextHolder {\n\n    private final SerializationContext capturedContext;\n\n    protected DynamicOpsWithContext(SerializationContext capturedContext, DynamicOps<T> delegate) {\n        super(delegate);\n\n        this.capturedContext = capturedContext;\n    }\n\n    public static <T> DynamicOpsWithContext<T> of(SerializationContext context, DynamicOps<T> delegate) {\n        return new DynamicOpsWithContext<>(context, delegate);\n    }\n\n    public static <T> DynamicOpsWithContext<T> ofEmptyContext(DynamicOps<T> delegate) {\n        return new DynamicOpsWithContext<>(SerializationContext.empty(), delegate);\n    }\n\n    @Override\n    public SerializationContext capturedContext() {\n        return this.capturedContext;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/format/edm/EdmOps.java",
    "content": "package io.wispforest.owo.serialization.format.edm;\n\nimport com.mojang.datafixers.DataFixUtils;\nimport com.mojang.datafixers.util.Pair;\nimport com.mojang.serialization.DataResult;\nimport com.mojang.serialization.DynamicOps;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.format.edm.EdmElement;\nimport io.wispforest.owo.serialization.format.ContextHolder;\n\nimport java.nio.ByteBuffer;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class EdmOps implements DynamicOps<EdmElement<?>>, ContextHolder {\n\n    private static final EdmOps NO_CONTEXT = new EdmOps(SerializationContext.empty());\n\n    private final SerializationContext capturedContext;\n    private EdmOps(SerializationContext capturedContext) {\n        this.capturedContext = capturedContext;\n    }\n\n    public static EdmOps withContext(SerializationContext context) {\n        return new EdmOps(context);\n    }\n\n    public static EdmOps withoutContext() {\n        return NO_CONTEXT;\n    }\n\n    @Override\n    public SerializationContext capturedContext() {\n        return this.capturedContext;\n    }\n\n    // --- Serialization ---\n\n    @Override\n    public EdmElement<?> empty() {\n        return EdmElement.EMPTY;\n    }\n\n    public EdmElement<?> createNumeric(Number number) {\n        return EdmElement.f64(number.doubleValue());\n    }\n\n    public EdmElement<?> createByte(byte b) {\n        return EdmElement.i8(b);\n    }\n\n    public EdmElement<?> createShort(short s) {\n        return EdmElement.i16(s);\n    }\n\n    public EdmElement<?> createInt(int i) {\n        return EdmElement.i32(i);\n    }\n\n    public EdmElement<?> createLong(long l) {\n        return EdmElement.i64(l);\n    }\n\n    public EdmElement<?> createFloat(float f) {\n        return EdmElement.f32(f);\n    }\n\n    public EdmElement<?> createDouble(double d) {\n        return EdmElement.f64(d);\n    }\n\n    // ---\n\n    public EdmElement<?> createBoolean(boolean bl) {\n        return EdmElement.bool(bl);\n    }\n\n    @Override\n    public EdmElement<?> createString(String value) {\n        return EdmElement.string(value);\n    }\n\n    @Override\n    public EdmElement<?> createByteList(ByteBuffer input) {\n        return EdmElement.bytes(DataFixUtils.toArray(input));\n    }\n\n    // ---\n\n    @Override\n    public EdmElement<?> createList(Stream<EdmElement<?>> input) {\n        return EdmElement.sequence(input.toList());\n    }\n\n    @Override\n    public DataResult<EdmElement<?>> mergeToList(EdmElement<?> list, EdmElement<?> value) {\n        if (list == empty()) {\n            return DataResult.success(EdmElement.sequence(List.of(value)));\n        } else if (list.value() instanceof List<?> properList) {\n            var newList = new ArrayList<EdmElement<?>>((Collection<? extends EdmElement<?>>) properList);\n            newList.add(value);\n\n            return DataResult.success(EdmElement.sequence(newList));\n        } else {\n            return DataResult.error(() -> \"Not a sequence: \" + list);\n        }\n    }\n\n    @Override\n    public EdmElement<?> createMap(Stream<Pair<EdmElement<?>, EdmElement<?>>> map) {\n        return EdmElement.consumeMap(map.collect(Collectors.toMap(pair -> pair.getFirst().cast(), Pair::getSecond)));\n    }\n\n    @Override\n    public DataResult<EdmElement<?>> mergeToMap(EdmElement<?> map, EdmElement<?> key, EdmElement<?> value) {\n        if (!(key.value() instanceof String)) {\n            return DataResult.error(() -> \"Key is not a string: \" + key);\n        }\n\n        if (map == empty()) {\n            return DataResult.success(EdmElement.consumeMap(Map.of(key.cast(), value)));\n        } else if (map.value() instanceof Map<?, ?> properMap) {\n            var newMap = new HashMap<String, EdmElement<?>>((Map<String, ? extends EdmElement<?>>) properMap);\n            newMap.put(key.cast(), value);\n\n            return DataResult.success(EdmElement.consumeMap(newMap));\n        } else {\n            return DataResult.error(() -> \"Not a map: \" + map);\n        }\n    }\n\n    // --- Deserialization ---\n\n    @Override\n    public DataResult<Number> getNumberValue(EdmElement<?> input) {\n        if (input.value() instanceof Number number) {\n            return DataResult.success(number);\n        } else {\n            return DataResult.error(() -> \"Not a number: \" + input);\n        }\n    }\n\n    @Override\n    public DataResult<Boolean> getBooleanValue(EdmElement<?> input) {\n        if (input.value() instanceof Boolean bl) {\n            return DataResult.success(bl);\n        } else if(input.value() instanceof Byte b) {\n            return DataResult.success(b == 1);\n        } else {\n            return DataResult.error(() -> \"Not a boolean: \" + input);\n        }\n    }\n\n    @Override\n    public DataResult<String> getStringValue(EdmElement<?> input) {\n        if (input.value() instanceof String string) {\n            return DataResult.success(string);\n        } else {\n            return DataResult.error(() -> \"Not a string: \" + input);\n        }\n    }\n\n    @Override\n    public DataResult<ByteBuffer> getByteBuffer(EdmElement<?> input) {\n        if (input.value() instanceof byte[] bytes) {\n            return DataResult.success(ByteBuffer.wrap(bytes));\n        } else {\n            return DataResult.error(() -> \"Not bytes: \" + input);\n        }\n    }\n\n    // ---\n\n    @Override\n    public DataResult<Stream<EdmElement<?>>> getStream(EdmElement<?> input) {\n        if (input == this.empty()) {\n            return DataResult.success(Stream.of());\n        } else if (input.value() instanceof List<?> list) {\n            return DataResult.success(list.stream().map(o -> (EdmElement<?>) o));\n        } else {\n            return DataResult.error(() -> \"Not a sequence: \" + input);\n        }\n    }\n\n    @Override\n    public DataResult<Stream<Pair<EdmElement<?>, EdmElement<?>>>> getMapValues(EdmElement<?> input) {\n        if (input == this.empty()) {\n            return DataResult.success(Stream.of());\n        } else if (input.value() instanceof Map<?, ?> map) {\n            //noinspection rawtypes\n            return DataResult.success(map.entrySet().stream().map(entry -> new Pair(EdmElement.string((String) entry.getKey()), entry.getValue())));\n        } else {\n            return DataResult.error(() -> \"Not a map: \" + input);\n        }\n    }\n\n    // ---\n\n    @Override\n    public <U> U convertTo(DynamicOps<U> outOps, EdmElement<?> input) {\n        if (input == this.empty()) return outOps.empty();\n        return switch (input.type()) { // TODO: DO WE NEED TO HANDLE Unsigned Numbers specifically here or nah?\n            case I8, U8 -> outOps.createByte(input.cast());\n            case I16, U16 -> outOps.createShort(input.cast());\n            case I32, U32 -> outOps.createInt(input.cast());\n            case I64, U64 -> outOps.createLong(input.cast());\n            case F32 -> outOps.createFloat(input.cast());\n            case F64 -> outOps.createDouble(input.cast());\n            case BOOLEAN -> outOps.createBoolean(input.cast());\n            case STRING -> outOps.createString(input.cast());\n            case BYTES -> outOps.createByteList(ByteBuffer.wrap(input.cast()));\n            case OPTIONAL -> input.<Optional<EdmElement<?>>>cast().map(element -> this.convertTo(outOps, element)).orElse(outOps.empty());\n            case SEQUENCE -> outOps.createList(input.<List<EdmElement<?>>>cast().stream().map(element -> this.convertTo(outOps, element)));\n            case MAP ->\n                    outOps.createMap(input.<Map<String, EdmElement<?>>>cast().entrySet().stream().map(entry -> new Pair<>(outOps.createString(entry.getKey()), this.convertTo(outOps, entry.getValue()))));\n        };\n    }\n\n    @Override\n    public EdmElement<?> remove(EdmElement<?> input, String key) {\n        if (input.value() instanceof Map<?, ?> map) {\n            var newMap = new HashMap<String, EdmElement<?>>((Map<? extends String, ? extends EdmElement<?>>) map);\n            newMap.remove(key);\n\n            return EdmElement.consumeMap(newMap);\n        } else {\n            return input;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/format/nbt/NbtDeserializer.java",
    "content": "package io.wispforest.owo.serialization.format.nbt;\n\nimport com.google.common.collect.MapMaker;\nimport io.wispforest.endec.*;\nimport io.wispforest.endec.format.edm.EdmElement;\nimport io.wispforest.endec.util.RecursiveDeserializer;\nimport net.minecraft.nbt.*;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.*;\nimport java.util.function.Supplier;\n\npublic class NbtDeserializer extends RecursiveDeserializer<Tag> implements SelfDescribedDeserializer<Tag> {\n\n    protected NbtDeserializer(Tag element) {\n        super(element);\n    }\n\n    public static NbtDeserializer of(Tag element) {\n        return new NbtDeserializer(element);\n    }\n\n    private <N extends Tag> N getAs(SerializationContext ctx, Tag element, Class<N> clazz) {\n        if (!clazz.isInstance(element)) {\n            ctx.throwMalformedInput(\"Expected a \" + clazz.getSimpleName() + \", found a \" + element.getClass().getSimpleName());\n        }\n\n        return clazz.cast(element);\n    }\n\n    // ---\n\n    @Override\n    public byte readByte(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), ByteTag.class).byteValue();\n    }\n\n    @Override\n    public short readShort(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), ShortTag.class).shortValue();\n    }\n\n    @Override\n    public int readInt(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), IntTag.class).intValue();\n    }\n\n    @Override\n    public long readLong(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), LongTag.class).longValue();\n    }\n\n    @Override\n    public float readFloat(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), FloatTag.class).floatValue();\n    }\n\n    @Override\n    public double readDouble(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), DoubleTag.class).doubleValue();\n    }\n\n    // ---\n\n    @Override\n    public int readVarInt(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), NumericTag.class).intValue();\n    }\n\n    @Override\n    public long readVarLong(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), NumericTag.class).longValue();\n    }\n\n    // ---\n\n    @Override\n    public boolean readBoolean(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), ByteTag.class).byteValue() != 0;\n    }\n\n    @Override\n    public String readString(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), StringTag.class).asString().get();\n    }\n\n    @Override\n    public byte[] readBytes(SerializationContext ctx) {\n        return this.getAs(ctx, this.getValue(), ByteArrayTag.class).getAsByteArray();\n    }\n\n    private final Set<Tag> encodedOptionals = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap());\n\n    @Override\n    public <V> Optional<V> readOptional(SerializationContext ctx, Endec<V> endec) {\n        var value = this.getValue();\n        if (this.encodedOptionals.contains(value)) {\n            return Optional.of(endec.decode(ctx, this));\n        }\n\n        var struct = this.struct(ctx);\n        return struct.field(\"present\", ctx, Endec.BOOLEAN)\n                ? Optional.of(struct.field(\"value\", ctx, endec))\n                : Optional.empty();\n    }\n\n    // ---\n\n    @Override\n    public <E> Deserializer.Sequence<E> sequence(SerializationContext ctx, Endec<E> elementEndec) {\n        //noinspection unchecked\n        var list = this.getAs(ctx, this.getValue(), CollectionTag.class);\n        return new Sequence<E>(ctx, elementEndec, list, list.size());\n    }\n\n    @Override\n    public <V> Deserializer.Map<V> map(SerializationContext ctx, Endec<V> valueEndec) {\n        return new Map<>(ctx, valueEndec, this.getAs(ctx, this.getValue(), CompoundTag.class));\n    }\n\n    @Override\n    public Deserializer.Struct struct(SerializationContext ctx) {\n        return new Struct(this.getAs(ctx, this.getValue(), CompoundTag.class));\n    }\n\n    // ---\n\n    @Override\n    public <S> void readAny(SerializationContext ctx, Serializer<S> visitor) {\n        this.decodeValue(ctx, visitor, this.getValue());\n    }\n\n    private <S> void decodeValue(SerializationContext ctx, Serializer<S> visitor, Tag value) {\n        switch (value.getId()) {\n            case Tag.TAG_BYTE -> visitor.writeByte(ctx, ((ByteTag) value).byteValue());\n            case Tag.TAG_SHORT -> visitor.writeShort(ctx, ((ShortTag) value).shortValue());\n            case Tag.TAG_INT -> visitor.writeInt(ctx, ((IntTag) value).intValue());\n            case Tag.TAG_LONG -> visitor.writeLong(ctx, ((LongTag) value).longValue());\n            case Tag.TAG_FLOAT -> visitor.writeFloat(ctx, ((FloatTag) value).floatValue());\n            case Tag.TAG_DOUBLE -> visitor.writeDouble(ctx, ((DoubleTag) value).doubleValue());\n            case Tag.TAG_STRING -> visitor.writeString(ctx, value.asString().get());\n            case Tag.TAG_BYTE_ARRAY -> visitor.writeBytes(ctx, ((ByteArrayTag) value).getAsByteArray());\n            case Tag.TAG_INT_ARRAY, Tag.TAG_LONG_ARRAY, Tag.TAG_LIST -> {\n                var list = (CollectionTag) value;\n                try (var sequence = visitor.sequence(ctx, Endec.<Tag>of(this::decodeValue, (ctx1, deserializer) -> null), list.size())) {\n                    list.forEach(sequence::element);\n                }\n            }\n            case Tag.TAG_COMPOUND -> {\n                var compound = (CompoundTag) value;\n                try (var map = visitor.map(ctx, Endec.<Tag>of(this::decodeValue, (ctx1, deserializer) -> null), compound.size())) {\n                    for (var key : compound.keySet()) {\n                        map.entry(key, compound.get(key));\n                    }\n                }\n            }\n            default ->\n                    throw new IllegalArgumentException(\"Non-standard, unrecognized NbtElement implementation cannot be decoded\");\n        }\n    }\n\n    // ---\n\n    private class Sequence<V> implements Deserializer.Sequence<V> {\n\n        private final SerializationContext ctx;\n        private final Endec<V> valueEndec;\n        private final Iterator<Tag> elements;\n        private final int size;\n\n        private Sequence(SerializationContext ctx, Endec<V> valueEndec, Iterable<Tag> elements, int size) {\n            this.ctx = ctx;\n            this.valueEndec = valueEndec;\n\n            this.elements = elements.iterator();\n            this.size = size;\n        }\n\n        @Override\n        public int estimatedSize() {\n            return this.size;\n        }\n\n        @Override\n        public boolean hasNext() {\n            return this.elements.hasNext();\n        }\n\n        @Override\n        public V next() {\n            var value = this.elements.next();\n\n            return NbtDeserializer.this.frame(\n                    () -> value,\n                    () -> this.valueEndec.decode(this.ctx, NbtDeserializer.this)\n            );\n        }\n    }\n\n    private class Map<V> implements Deserializer.Map<V> {\n\n        private final SerializationContext ctx;\n        private final Endec<V> valueEndec;\n        private final CompoundTag compound;\n        private final Iterator<String> keys;\n        private final int size;\n\n        private Map(SerializationContext ctx, Endec<V> valueEndec, CompoundTag compound) {\n            this.ctx = ctx;\n            this.valueEndec = valueEndec;\n\n            this.compound = compound;\n            this.keys = compound.keySet().iterator();\n            this.size = compound.size();\n        }\n\n        @Override\n        public int estimatedSize() {\n            return this.size;\n        }\n\n        @Override\n        public boolean hasNext() {\n            return this.keys.hasNext();\n        }\n\n        @Override\n        public java.util.Map.Entry<String, V> next() {\n            var key = this.keys.next();\n            return NbtDeserializer.this.frame(\n                    () -> this.compound.get(key),\n                    () -> java.util.Map.entry(key, this.valueEndec.decode(this.ctx, NbtDeserializer.this))\n            );\n        }\n    }\n\n    public class Struct implements Deserializer.Struct {\n\n        private final CompoundTag compound;\n\n        public Struct(CompoundTag compound) {\n            this.compound = compound;\n        }\n\n        @Override\n        public <F> @Nullable F field(String name, SerializationContext ctx, Endec<F> endec, @Nullable Supplier<F> defaultValueFactory) {\n            if (!this.compound.contains(name)) {\n                if (defaultValueFactory == null) {\n                    throw new IllegalStateException(\"Field '\" + name + \"' was missing from serialized data, but no default value was provided\");\n                }\n\n                return defaultValueFactory.get();\n            }\n            var element = this.compound.get(name);\n            if (defaultValueFactory != null) NbtDeserializer.this.encodedOptionals.add(element);\n            return NbtDeserializer.this.frame(\n                    () -> element,\n                    () -> endec.decode(ctx, NbtDeserializer.this)\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/format/nbt/NbtEndec.java",
    "content": "package io.wispforest.owo.serialization.format.nbt;\n\nimport com.google.common.io.ByteStreams;\nimport io.wispforest.endec.*;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.NbtAccounter;\nimport net.minecraft.nbt.NbtIo;\nimport net.minecraft.nbt.Tag;\n\nimport java.io.IOException;\n\npublic final class NbtEndec implements Endec<Tag> {\n\n    public static final Endec<Tag> ELEMENT = new NbtEndec();\n    public static final Endec<CompoundTag> COMPOUND = new NbtEndec().xmap(CompoundTag.class::cast, compound -> compound);\n\n    private NbtEndec() {}\n\n    @Override\n    public void encode(SerializationContext ctx, Serializer<?> serializer, Tag value) {\n        if (serializer instanceof SelfDescribedSerializer<?>) {\n            NbtDeserializer.of(value).readAny(ctx, serializer);\n            return;\n        }\n\n        try {\n            var output = ByteStreams.newDataOutput();\n            NbtIo.writeAnyTag(value, output);\n\n            serializer.writeBytes(ctx, output.toByteArray());\n        } catch (IOException e) {\n            throw new RuntimeException(\"Failed to encode binary NBT in NbtEndec\", e);\n        }\n    }\n\n    @Override\n    public Tag decode(SerializationContext ctx, Deserializer<?> deserializer) {\n        if (deserializer instanceof SelfDescribedDeserializer<?> selfDescribedDeserializer) {\n            var nbt = NbtSerializer.of();\n            selfDescribedDeserializer.readAny(ctx, nbt);\n\n            return nbt.result();\n        }\n\n        try {\n            return NbtIo.readAnyTag(ByteStreams.newDataInput(deserializer.readBytes(ctx)), NbtAccounter.unlimitedHeap());\n        } catch (IOException e) {\n            throw new RuntimeException(\"Failed to parse binary NBT in NbtEndec\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/serialization/format/nbt/NbtSerializer.java",
    "content": "package io.wispforest.owo.serialization.format.nbt;\n\nimport com.google.common.collect.MapMaker;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SelfDescribedSerializer;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.Serializer;\nimport io.wispforest.endec.util.RecursiveSerializer;\nimport net.minecraft.nbt.*;\nimport net.minecraft.network.VarInt;\nimport net.minecraft.network.VarLong;\nimport org.apache.commons.lang3.mutable.MutableObject;\n\nimport java.util.Collections;\nimport java.util.Optional;\nimport java.util.Set;\n\npublic class NbtSerializer extends RecursiveSerializer<Tag> implements SelfDescribedSerializer<Tag> {\n\n    protected Tag prefix;\n\n    protected NbtSerializer(Tag prefix) {\n        super(EndTag.INSTANCE);\n        this.prefix = prefix;\n    }\n\n    public static NbtSerializer of(Tag prefix) {\n        return new NbtSerializer(prefix);\n    }\n\n    public static NbtSerializer of() {\n        return of(null);\n    }\n\n    // ---\n\n    @Override\n    public void writeByte(SerializationContext ctx, byte value) {\n        this.consume(ByteTag.valueOf(value));\n    }\n\n    @Override\n    public void writeShort(SerializationContext ctx, short value) {\n        this.consume(ShortTag.valueOf(value));\n    }\n\n    @Override\n    public void writeInt(SerializationContext ctx, int value) {\n        this.consume(IntTag.valueOf(value));\n    }\n\n    @Override\n    public void writeLong(SerializationContext ctx, long value) {\n        this.consume(LongTag.valueOf(value));\n    }\n\n    @Override\n    public void writeFloat(SerializationContext ctx, float value) {\n        this.consume(FloatTag.valueOf(value));\n    }\n\n    @Override\n    public void writeDouble(SerializationContext ctx, double value) {\n        this.consume(DoubleTag.valueOf(value));\n    }\n\n    // ---\n\n    @Override\n    public void writeVarInt(SerializationContext ctx, int value) {\n        this.consume(switch (VarInt.getByteSize(value)) {\n            case 0, 1 -> ByteTag.valueOf((byte) value);\n            case 2 -> ShortTag.valueOf((short) value);\n            default -> IntTag.valueOf(value);\n        });\n    }\n\n    @Override\n    public void writeVarLong(SerializationContext ctx, long value) {\n        this.consume(switch (VarLong.getByteSize(value)) {\n            case 0, 1 -> ByteTag.valueOf((byte) value);\n            case 2 -> ShortTag.valueOf((short) value);\n            case 3, 4 -> IntTag.valueOf((int) value);\n            default -> LongTag.valueOf(value);\n        });\n    }\n\n    // ---\n\n    @Override\n    public void writeBoolean(SerializationContext ctx, boolean value) {\n        this.consume(ByteTag.valueOf(value));\n    }\n\n    @Override\n    public void writeString(SerializationContext ctx, String value) {\n        this.consume(StringTag.valueOf(value));\n    }\n\n    @Override\n    public void writeBytes(SerializationContext ctx, byte[] bytes) {\n        this.consume(new ByteArrayTag(bytes));\n    }\n\n    private final Set<Tag> encodedOptionals = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap());\n\n    @Override\n    public <V> void writeOptional(SerializationContext ctx, Endec<V> endec, Optional<V> optional) {\n        MutableObject<Tag> frameData = new MutableObject<>();\n\n        this.frame(encoded -> {\n            try (var struct = this.struct()) {\n                struct.field(\"present\", ctx, Endec.BOOLEAN, optional.isPresent());\n                optional.ifPresent(value -> struct.field(\"value\", ctx, endec, value));\n            }\n\n            var compound = encoded.require(\"optional representation\");\n\n            encodedOptionals.add(compound);\n            frameData.setValue(compound);\n        });\n\n        this.consume(frameData.getValue());\n    }\n\n    // ---\n\n    @Override\n    public <E> Serializer.Sequence<E> sequence(SerializationContext ctx, Endec<E> elementEndec, int size) {\n        return new Sequence<>(ctx, elementEndec);\n    }\n\n    @Override\n    public <V> Serializer.Map<V> map(SerializationContext ctx, Endec<V> valueEndec, int size) {\n        return new Map<>(ctx, valueEndec);\n    }\n\n    @Override\n    public Struct struct() {\n        return new Map<>(null, null);\n    }\n\n    // ---\n\n    private class Map<V> implements Serializer.Map<V>, Struct {\n\n        private final SerializationContext ctx;\n        private final Endec<V> valueEndec;\n        private final CompoundTag result;\n\n        private Map(SerializationContext ctx, Endec<V> valueEndec) {\n            this.ctx = ctx;\n            this.valueEndec = valueEndec;\n\n            if (NbtSerializer.this.prefix != null) {\n                if (NbtSerializer.this.prefix instanceof CompoundTag prefixMap) {\n                    this.result = prefixMap;\n                } else {\n                    throw new IllegalStateException(\"Incompatible prefix of type \" + NbtSerializer.this.prefix.getClass().getSimpleName() + \" provided for NBT map/struct\");\n                }\n            } else {\n                this.result = new CompoundTag();\n            }\n        }\n\n        @Override\n        public void entry(String key, V value) {\n            NbtSerializer.this.frame(encoded -> {\n                this.valueEndec.encode(this.ctx, NbtSerializer.this, value);\n                this.result.put(key, encoded.require(\"map value\"));\n            });\n        }\n\n        @Override\n        public <F> Struct field(String name, SerializationContext ctx, Endec<F> endec, F value, boolean mayOmit) {\n            NbtSerializer.this.frame(encoded -> {\n                endec.encode(ctx, NbtSerializer.this, value);\n\n                var element = encoded.require(\"struct field\");\n\n                if (mayOmit && NbtSerializer.this.encodedOptionals.contains(element)) {\n                    var nbtCompound = (CompoundTag) element;\n\n                    if(!nbtCompound.getBooleanOr(\"present\", false)) return;\n\n                    element = nbtCompound.get(\"value\");\n                }\n\n                this.result.put(name, element);\n            });\n\n            return this;\n        }\n\n        @Override\n        public void end() {\n            NbtSerializer.this.consume(this.result);\n        }\n    }\n\n    private class Sequence<V> implements Serializer.Sequence<V> {\n\n        private final SerializationContext ctx;\n        private final Endec<V> valueEndec;\n        private final ListTag result;\n\n        private Sequence(SerializationContext ctx, Endec<V> valueEndec) {\n            this.ctx = ctx;\n            this.valueEndec = valueEndec;\n\n            if (NbtSerializer.this.prefix != null) {\n                if (NbtSerializer.this.prefix instanceof ListTag prefixList) {\n                    this.result = prefixList;\n                } else {\n                    throw new IllegalStateException(\"Incompatible prefix of type \" + NbtSerializer.this.prefix.getClass().getSimpleName() + \" provided for NBT sequence\");\n                }\n            } else {\n                this.result = new ListTag();\n            }\n        }\n\n        @Override\n        public void element(V element) {\n            NbtSerializer.this.frame(encoded -> {\n                this.valueEndec.encode(this.ctx, NbtSerializer.this, element);\n                this.result.add(encoded.require(\"sequence element\"));\n            });\n        }\n\n        @Override\n        public void end() {\n            NbtSerializer.this.consume(this.result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/text/CursedTranslatableContents.java",
    "content": "package io.wispforest.owo.text;\n\nimport io.wispforest.owo.mixin.text.TranslatableContentsAccessor;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.ComponentContents;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.chat.contents.PlainTextContents;\nimport net.minecraft.network.chat.contents.TranslatableContents;\n\nimport java.util.ArrayList;\n\npublic class CursedTranslatableContents extends TranslatableContents {\n    public static int argIndex = 0;\n\n    private static final CursedTranslatableContents INSTANCE = new CursedTranslatableContents();\n\n    private CursedTranslatableContents() {\n        super(\"\", null, null);\n    }\n\n    public static Component unpackArgs(Component text) {\n        argIndex = 0;\n        var returned = unpack(text);\n        argIndex = 0;\n        return returned;\n    }\n\n    private static Component unpack(Component text) {\n        var unpacked = new ArrayList<Component>();\n        ComponentContents newContent = PlainTextContents.EMPTY;\n        if (text.getContents() instanceof PlainTextContents.LiteralContents(String string)) {\n            ((TranslatableContentsAccessor) INSTANCE).owo$decomposeTemplate(\n                string,\n                part -> {\n                    if (part instanceof Component textPart) unpacked.add(textPart);\n                    else unpacked.add(Component.literal(part.getString()));\n                }\n            );\n        } else {\n            if (text.getSiblings().isEmpty()) return text;\n            newContent = text.getContents();\n        }\n        var newText = MutableComponent.create(newContent).setStyle(text.getStyle());\n        for (var part : unpacked) newText.append(part);\n        for (var child : text.getSiblings()) newText.append(unpack(child));\n        return newText;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/text/CustomTextRegistry.java",
    "content": "package io.wispforest.owo.text;\n\nimport com.mojang.serialization.MapCodec;\nimport net.minecraft.network.chat.ComponentContents;\nimport net.minecraft.util.ExtraCodecs;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic final class CustomTextRegistry {\n\n    private static final Map<String, MapCodec<? extends ComponentContents>> TYPES = new HashMap<>();\n    private static ExtraCodecs.LateBoundIdMapper<String, MapCodec<? extends ComponentContents>> codecIdMapper;\n\n    private CustomTextRegistry() {}\n\n    public static void register(String triggerField, MapCodec<? extends ComponentContents> codec) {\n        TYPES.put(triggerField, codec);\n        if (codecIdMapper != null) {\n            codecIdMapper.put(triggerField, codec);\n        }\n    }\n\n    @ApiStatus.Internal\n    public static void inject(ExtraCodecs.LateBoundIdMapper<String, MapCodec<? extends ComponentContents>> mapper) {\n        TYPES.forEach(mapper::put);\n        codecIdMapper = mapper;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/text/InsertingTextContent.java",
    "content": "package io.wispforest.owo.text;\n\nimport com.mojang.serialization.MapCodec;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.ComponentContents;\nimport net.minecraft.network.chat.FormattedText;\nimport net.minecraft.network.chat.Style;\n\nimport java.util.Optional;\n\npublic record InsertingTextContent(int index) implements ComponentContents {\n\n    public static final MapCodec<InsertingTextContent> CODEC = CodecUtils.toMapCodec(StructEndecBuilder.of(Endec.INT.fieldOf(\"index\", InsertingTextContent::index), InsertingTextContent::new));\n\n    @Override\n    public <T> Optional<T> visit(FormattedText.ContentConsumer<T> visitor) {\n        var current = TranslationContext.getCurrent();\n\n        if (current == null || current.getArgs().length <= index) {return visitor.accept(\"%\" + (index + 1) + \"$s\");}\n\n        Object arg = current.getArgs()[index];\n\n        if (arg instanceof Component text) {\n            return text.visit(visitor);\n        } else {\n            return visitor.accept(arg.toString());\n        }\n    }\n\n    @Override\n    public <T> Optional<T> visit(FormattedText.StyledContentConsumer<T> visitor, Style style) {\n        var current = TranslationContext.getCurrent();\n\n        if (current == null || current.getArgs().length <= index) {\n            return visitor.accept(style, \"%\" + (index + 1) + \"$s\");\n        }\n\n        Object arg = current.getArgs()[index];\n\n        if (arg instanceof Component text) {\n            return text.visit(visitor, style);\n        } else {\n            return visitor.accept(style, arg.toString());\n        }\n    }\n\n    @Override\n    public MapCodec<? extends ComponentContents> codec() {\n        return CODEC;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/text/LanguageAccess.java",
    "content": "package io.wispforest.owo.text;\n\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.function.BiConsumer;\n\n@ApiStatus.Internal\npublic class LanguageAccess {\n    public static final BiConsumer<String, Component> EMPTY_CONSUMER = (string, component) -> {};\n\n    public static ThreadLocal<BiConsumer<String, Component>> textConsumer = ThreadLocal.withInitial(() -> EMPTY_CONSUMER);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/text/NestedLangHandler.java",
    "content": "package io.wispforest.owo.text;\n\nimport com.google.gson.JsonElement;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.regex.Pattern;\n\n@ApiStatus.Internal\npublic class NestedLangHandler {\n    private static final Pattern NESTED_OBJECT_PATTERN = Pattern.compile(\"^(.*?)\\\\{}(.*?)$\");\n    private static final Pattern NESTED_LIST_PATTERN = Pattern.compile(\"^(.*?)\\\\{((?:-?[0-9]*)?)}(.*?)$\");\n    private static final Pattern EMPTY_STRIP_PATTERN = Pattern.compile(\"[^a-zA-Z0-9]+$\");\n\n    public static Set<Map.Entry<String, JsonElement>> deNest(Set<Map.Entry<String, JsonElement>> entries) {\n        return deNest(\"\", entries, \"\");\n    }\n\n    private static Set<Map.Entry<String, JsonElement>> deNest(\n        String prefix,\n        Set<Map.Entry<String, JsonElement>> entries,\n        String suffix\n    ) {\n        var returned = new HashSet<Map.Entry<String, JsonElement>>();\n        for (var entry : entries) {\n            var key = entry.getKey();\n            var value = entry.getValue();\n\n            var objectMatcher = NESTED_OBJECT_PATTERN.matcher(key);\n            var listMatcher = NESTED_LIST_PATTERN.matcher(key);\n            if (value.isJsonObject() && objectMatcher.matches()) {\n                returned.addAll(deNest(\n                    prefix + objectMatcher.group(1),\n                    value.getAsJsonObject().entrySet(),\n                    objectMatcher.group(2) + suffix\n                ));\n            } else if (value.isJsonArray() && listMatcher.matches()) {\n                var start = Mth.getInt(listMatcher.group(2), 1);\n                var array = value.getAsJsonArray();\n                for (int i = 0; i < array.size(); i++) {\n                    returned.addAll(deNest(\n                        prefix + listMatcher.group(1),\n                        Set.of(Map.entry(String.valueOf((start + i)), array.get(i))),\n                        listMatcher.group(3) + suffix\n                    ));\n                }\n            } else {\n                returned.add(Map.entry(\n                    key.isEmpty()\n                        ? prefix.replaceAll(EMPTY_STRIP_PATTERN.pattern(), \"\") + suffix\n                        : prefix + key + suffix, value\n                ));\n            }\n        }\n        return returned;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/text/TextLanguage.java",
    "content": "package io.wispforest.owo.text;\n\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.Nullable;\n\npublic interface TextLanguage {\n    @Nullable Component getText(String key);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/text/TranslationContext.java",
    "content": "package io.wispforest.owo.text;\n\nimport net.minecraft.network.chat.contents.TranslatableContents;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TranslationContext {\n    private static final ThreadLocal<List<TranslatableContents>> translationStack = ThreadLocal.withInitial(ArrayList::new);\n\n    public static boolean pushContent(TranslatableContents content) {\n        var stack = translationStack.get();\n\n        for (int i = 0; i < stack.size(); i++) {\n            if (stack.get(i) == content)\n                return false;\n        }\n\n        stack.add(content);\n\n        return true;\n    }\n\n    public static void popContent() {\n        var stack = translationStack.get();\n\n        stack.remove(stack.size() - 1);\n    }\n\n    public static TranslatableContents getCurrent() {\n        var stack = translationStack.get();\n\n        if (stack.isEmpty())\n            return null;\n        else\n            return stack.get(stack.size() - 1);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseOwoContainerScreen.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.ui.SlotAccessor;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.inject.GreedyInputUIComponent;\nimport io.wispforest.owo.ui.util.DisposableScreen;\nimport io.wispforest.owo.ui.util.UIErrorToast;\nimport io.wispforest.owo.util.pond.OwoSlotExtension;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.inventory.Slot;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.opengl.GL11;\n\nimport java.util.ArrayList;\nimport java.util.Optional;\nimport java.util.function.BiFunction;\nimport java.util.stream.Stream;\n\npublic abstract class BaseOwoContainerScreen<R extends ParentUIComponent, S extends AbstractContainerMenu> extends AbstractContainerScreen<S> implements DisposableScreen {\n\n    /**\n     * The UI adapter of this screen. This handles\n     * all user input as well as setting up GL state for rendering\n     * and managing component focus\n     */\n    protected OwoUIAdapter<R> uiAdapter = null;\n\n    /**\n     * Whether this screen has encountered an unrecoverable\n     * error during its lifecycle and should thus close\n     * itself on the next frame\n     */\n    protected boolean invalid = false;\n\n    protected BaseOwoContainerScreen(S menu, Inventory inventory, Component title) {\n        super(menu, inventory, title);\n    }\n\n    /**\n     * Initialize the UI adapter for this screen. Usually\n     * the body of this method will simply consist of a call\n     * to {@link OwoUIAdapter#create(Screen, BiFunction)}\n     *\n     * @return The UI adapter for this screen to use\n     */\n    protected abstract @NotNull OwoUIAdapter<R> createAdapter();\n\n    /**\n     * Build the component hierarchy of this screen,\n     * called after the adapter and root component have been\n     * initialized by {@link #createAdapter()}\n     *\n     * @param rootComponent The root component created\n     *                      in the previous initialization step\n     */\n    protected abstract void build(R rootComponent);\n\n    @Override\n    protected void init() {\n        super.init();\n\n        if (this.invalid) return;\n\n        // Check whether this screen was already initialized\n        if (this.uiAdapter != null) {\n            // If it was, only resize the adapter instead of recreating it - this preserves UI state\n            this.uiAdapter.moveAndResize(0, 0, this.width, this.height);\n            // Re-add it as a child to circumvent vanilla clearing them\n            this.addRenderableWidget(this.uiAdapter);\n        } else {\n            try {\n                this.uiAdapter = this.createAdapter();\n                this.build(this.uiAdapter.rootComponent);\n\n                this.uiAdapter.inflateAndMount();\n            } catch (Exception error) {\n                Owo.LOGGER.warn(\"Could not initialize owo screen\", error);\n                UIErrorToast.report(error);\n                this.invalid = true;\n            }\n        }\n\n        ScreenEvents.afterRender(this).register((screen, drawContext, mouseX, mouseY, tickDelta) -> {\n            this.drawComponentTooltip(drawContext, mouseX, mouseY, tickDelta);\n        });\n    }\n\n    /**\n     * Draw the tooltip of this screen's component tree, invoked\n     * by {@link ScreenEvents#afterRender(Screen)} so that tooltips are\n     * properly rendered above content\n     */\n    protected void drawComponentTooltip(GuiGraphics graphics, int mouseX, int mouseY, float tickDelta) {\n        if (this.uiAdapter != null) this.uiAdapter.drawTooltip(graphics, mouseX, mouseY, tickDelta);\n    }\n\n    /**\n     * Disable the slot at the given index. Note\n     * that this is hard override and the slot cannot\n     * re-enable itself\n     *\n     * @param index The index of the slot to disable\n     */\n    protected void disableSlot(int index) {\n        this.disableSlot(this.menu.slots.get(index));\n    }\n\n    /**\n     * Disable the given slot. Note that\n     * this is hard override and the slot cannot\n     * re-enable itself\n     */\n    protected void disableSlot(Slot slot) {\n        ((OwoSlotExtension) slot).owo$setDisabledOverride(true);\n    }\n\n    /**\n     * Enable the slot at the given index. Note\n     * that this is an override and cannot enable\n     * a slot that is disabled through its own will\n     *\n     * @param index The index of the slot to enable\n     */\n    protected void enableSlot(int index) {\n        this.enableSlot(this.menu.slots.get(index));\n    }\n\n    /**\n     * Enable the given slot. Note that\n     * this is an override and cannot enable\n     * a slot that is disabled through its own will\n     */\n    protected void enableSlot(Slot slot) {\n        ((OwoSlotExtension) slot).owo$setDisabledOverride(false);\n    }\n\n    /**\n     * @return whether the given slot is enabled or disabled\n     * using the {@link OwoSlotExtension} disabling functionality\n     */\n    protected boolean isSlotEnabled(int index) {\n        return isSlotEnabled(this.menu.slots.get(index));\n    }\n\n    /**\n     * @return whether the given slot is enabled or disabled\n     * using the {@link OwoSlotExtension} disabling functionality\n     */\n    protected boolean isSlotEnabled(Slot slot) {\n        return !((OwoSlotExtension) slot).owo$getDisabledOverride();\n    }\n\n    /**\n     * Wrap the slot at the given index in this screen's\n     * menu into a component, so it can be managed by the UI system\n     *\n     * @param index The index the slot occupies in the menu's slot list\n     * @return The wrapped slot\n     */\n    protected SlotComponent slotAsComponent(int index) {\n        return new SlotComponent(index);\n    }\n\n    /**\n     * A convenience shorthand for querying a component from the adapter's\n     * root component via {@link ParentUIComponent#childById(Class, String)}\n     */\n    protected <C extends UIComponent> C component(Class<C> expectedClass, String id) {\n        return this.uiAdapter.rootComponent.childById(expectedClass, id);\n    }\n\n    /**\n     * Compute a stream of all components for which to\n     * generate exclusion areas in a recipe viewer overlay.\n     * Called by the REI and EMI plugins\n     */\n    @ApiStatus.OverrideOnly\n    public Stream<UIComponent> componentsForExclusionAreas() {\n        if (this.children().isEmpty()) return Stream.of();\n\n        var rootComponent = uiAdapter.rootComponent;\n        var children = new ArrayList<UIComponent>();\n\n        rootComponent.collectDescendants(children);\n        children.remove(rootComponent);\n\n        return children.stream().filter(component -> !(component instanceof ParentUIComponent parent) || parent.surface() != Surface.BLANK);\n    }\n\n    @Override\n    public void renderBackground(GuiGraphics context, int mouseX, int mouseY, float delta) {}\n\n    @Override\n    public void render(GuiGraphics vanillaContext, int mouseX, int mouseY, float delta) {\n        var context = OwoUIGraphics.of(vanillaContext);\n        if (!this.invalid) {\n            super.render(context, mouseX, mouseY, delta);\n\n            if (this.uiAdapter.enableInspector) {\n                for (int i = 0; i < this.menu.slots.size(); i++) {\n                    var slot = this.menu.slots.get(i);\n                    if (!slot.isActive()) continue;\n\n                    context.drawText(Component.literal(\"H:\" + i),\n                        this.leftPos + slot.x + 15, this.topPos + slot.y + 9, .5f, 0x0096FF,\n                        OwoUIGraphics.TextAnchor.BOTTOM_RIGHT\n                    );\n                    context.drawText(Component.literal(\"I:\" + slot.getContainerSlot()),\n                        this.leftPos + slot.x + 15, this.topPos + slot.y + 15, .5f, 0x5800FF,\n                        OwoUIGraphics.TextAnchor.BOTTOM_RIGHT\n                    );\n                }\n            }\n\n            this.renderTooltip(context, mouseX, mouseY);\n        } else {\n            this.onClose();\n        }\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (!input.hasControlDown()\n            && this.uiAdapter.rootComponent.focusHandler().focused() instanceof GreedyInputUIComponent inputComponent\n            && inputComponent.onKeyPress(input)) {\n            return true;\n        }\n\n        return super.keyPressed(input);\n    }\n\n    @Override\n    public Optional<GuiEventListener> getChildAt(double mouseX, double mouseY) {\n        return super.getChildAt(mouseX, mouseY).flatMap(element -> element != this.uiAdapter ? Optional.of(element) : Optional.empty());\n    }\n\n    @Override\n    public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {\n        return this.uiAdapter.mouseClicked(click, doubled) || super.mouseClicked(click, doubled);\n    }\n\n    @Override\n    public boolean mouseDragged(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.uiAdapter.mouseDragged(click, deltaX, deltaY) || super.mouseDragged(click, deltaX, deltaY);\n    }\n\n    @Override\n    public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {\n        return this.uiAdapter.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) || super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);\n    }\n\n    @Nullable\n    @Override\n    public GuiEventListener getFocused() {\n        return this.uiAdapter;\n    }\n\n    @Override\n    public void removed() {\n        super.removed();\n        if (this.uiAdapter != null) {\n            this.uiAdapter.cursorAdapter.applyStyle(CursorStyle.NONE);\n        }\n    }\n\n    @Override\n    public void dispose() {\n        if (this.uiAdapter != null) this.uiAdapter.dispose();\n    }\n\n    @Override\n    protected void renderBg(GuiGraphics context, float delta, int mouseX, int mouseY) {}\n\n    public class SlotComponent extends BaseUIComponent {\n\n        protected final Slot slot;\n        protected boolean didDraw = false;\n\n        protected SlotComponent(int index) {\n            this.slot = BaseOwoContainerScreen.this.menu.getSlot(index);\n        }\n\n        @Override\n        public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n            this.didDraw = true;\n\n            int[] scissor = new int[4];\n            GL11.glGetIntegerv(GL11.GL_SCISSOR_BOX, scissor);\n\n            ((OwoSlotExtension) this.slot).owo$setScissorArea(PositionedRectangle.of(\n                scissor[0], scissor[1], scissor[2], scissor[3]\n            ));\n        }\n\n        @Override\n        public void update(float delta, int mouseX, int mouseY) {\n            super.update(delta, mouseX, mouseY);\n\n            ((OwoSlotExtension) this.slot).owo$setDisabledOverride(!this.didDraw);\n\n            this.didDraw = false;\n        }\n\n        @Override\n        public void drawTooltip(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {\n            if (!this.slot.hasItem()) {\n                super.drawTooltip(context, mouseX, mouseY, partialTicks, delta);\n            }\n        }\n\n        @Override\n        public boolean shouldDrawTooltip(double mouseX, double mouseY) {\n            return super.shouldDrawTooltip(mouseX, mouseY);\n        }\n\n        @Override\n        protected int determineHorizontalContentSize(Sizing sizing) {\n            return 16;\n        }\n\n        @Override\n        protected int determineVerticalContentSize(Sizing sizing) {\n            return 16;\n        }\n\n        @Override\n        public void updateX(int x) {\n            super.updateX(x);\n            ((SlotAccessor) this.slot).owo$setX(x - BaseOwoContainerScreen.this.leftPos);\n        }\n\n        @Override\n        public void updateY(int y) {\n            super.updateY(y);\n            ((SlotAccessor) this.slot).owo$setY(y - BaseOwoContainerScreen.this.topPos);\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseOwoScreen.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.OwoUIAdapter;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.ui.inject.GreedyInputUIComponent;\nimport io.wispforest.owo.ui.util.DisposableScreen;\nimport io.wispforest.owo.ui.util.UIErrorToast;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.BiFunction;\n\n/**\n * A minimal implementation of a Screen which fully\n * supports all aspects of the UI system. Implementing this class\n * is trivial, as you only need to provide implementations for\n * {@link #createAdapter()} to initialize the UI system and {@link #build(ParentUIComponent)}\n * which is where you declare your component hierarchy.\n * <p>\n * Should you be locked into a different superclass on your screen already,\n * you can easily copy all code from this class into your screen - as you\n * can see supporting the entire feature-set of owo-ui only requires\n * very few changes to how a vanilla screen works\n *\n * @param <R> The type of root component this screen uses\n */\npublic abstract class BaseOwoScreen<R extends ParentUIComponent> extends Screen implements DisposableScreen {\n\n    /**\n     * The UI adapter of this screen. This handles\n     * all user input as well as setting up GL state for rendering\n     * and managing component focus\n     */\n    protected OwoUIAdapter<R> uiAdapter = null;\n\n    /**\n     * Whether this screen has encountered an unrecoverable\n     * error during its lifecycle and should thus close\n     * itself on the next frame\n     */\n    protected boolean invalid = false;\n\n    protected BaseOwoScreen(Component title) {\n        super(title);\n    }\n\n    protected BaseOwoScreen() {\n        this(Component.empty());\n    }\n\n    /**\n     * Initialize the UI adapter for this screen. Usually\n     * the body of this method will simply consist of a call\n     * to {@link OwoUIAdapter#create(Screen, BiFunction)}\n     *\n     * @return The UI adapter for this screen to use\n     */\n    protected abstract @NotNull OwoUIAdapter<R> createAdapter();\n\n    /**\n     * Build the component hierarchy of this screen,\n     * called after the adapter and root component have been\n     * initialized by {@link #createAdapter()}\n     *\n     * @param rootComponent The root component created\n     *                      in the previous initialization step\n     */\n    protected abstract void build(R rootComponent);\n\n    @Override\n    protected void init() {\n        if (this.invalid) return;\n\n        // Check whether this screen was already initialized\n        if (this.uiAdapter != null) {\n            // If it was, only resize the adapter instead of recreating it - this preserves UI state\n            this.uiAdapter.moveAndResize(0, 0, this.width, this.height);\n            // Re-add it as a child to circumvent vanilla clearing them\n            this.addRenderableWidget(this.uiAdapter);\n        } else {\n            try {\n                this.uiAdapter = this.createAdapter();\n                this.build(this.uiAdapter.rootComponent);\n\n                this.uiAdapter.inflateAndMount();\n            } catch (Exception error) {\n                Owo.LOGGER.warn(\"Could not initialize owo screen\", error);\n                UIErrorToast.report(error);\n                this.invalid = true;\n            }\n        }\n\n        ScreenEvents.afterRender(this).register((screen, drawContext, mouseX, mouseY, tickDelta) -> {\n            this.drawComponentTooltip(drawContext, mouseX, mouseY, tickDelta);\n        });\n    }\n\n    /**\n     * Draw the tooltip of this screen's component tree, invoked\n     * by {@link ScreenEvents#afterRender(Screen)} so that tooltips are\n     * properly rendered above content\n     */\n    protected void drawComponentTooltip(GuiGraphics drawContext, int mouseX, int mouseY, float tickDelta) {\n        if (this.uiAdapter != null) this.uiAdapter.drawTooltip(drawContext, mouseX, mouseY, tickDelta);\n    }\n\n    /**\n     * A convenience shorthand for querying a component from the adapter's\n     * root component via {@link ParentUIComponent#childById(Class, String)}\n     */\n    protected <C extends UIComponent> C component(Class<C> expectedClass, String id) {\n        return this.uiAdapter.rootComponent.childById(expectedClass, id);\n    }\n\n    @Override\n    public void renderBackground(GuiGraphics context, int mouseX, int mouseY, float delta) {}\n\n    @Override\n    public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {\n        if (!this.invalid) {\n            super.render(context, mouseX, mouseY, delta);\n        } else {\n            this.onClose();\n        }\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (this.uiAdapter == null) return false;\n\n        if (!input.hasControlDown()\n                && this.uiAdapter.rootComponent.focusHandler().focused() instanceof GreedyInputUIComponent inputComponent\n                && inputComponent.onKeyPress(input)) {\n            return true;\n        }\n\n        if (super.keyPressed(input)) {\n            return true;\n        }\n\n        if (input.isEscape() && this.shouldCloseOnEsc()) {\n            this.onClose();\n            return true;\n        }\n\n        return false;\n    }\n\n    @Override\n    public boolean mouseDragged(MouseButtonEvent click, double deltaX, double deltaY) {\n        if (this.uiAdapter == null) return false;\n\n        return this.uiAdapter.mouseDragged(click, deltaX, deltaY);\n    }\n\n    @Nullable\n    @Override\n    public GuiEventListener getFocused() {\n        return this.uiAdapter;\n    }\n\n    @Override\n    public void removed() {\n        if (this.uiAdapter != null) {\n            this.uiAdapter.cursorAdapter.applyStyle(CursorStyle.NONE);\n        }\n    }\n\n    @Override\n    public void dispose() {\n        if (this.uiAdapter != null) this.uiAdapter.dispose();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseOwoToast.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Size;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.toasts.Toast;\nimport net.minecraft.client.gui.components.toasts.ToastManager;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.function.Supplier;\n\n@ApiStatus.Experimental\npublic abstract class BaseOwoToast<R extends ParentUIComponent> implements Toast {\n\n    protected final R rootComponent;\n    protected final VisibilityPredicate<R> visibilityPredicate;\n\n    protected int virtualWidth = 1000, virtualHeight = 1000;\n\n    protected BaseOwoToast(Supplier<R> components, VisibilityPredicate<R> predicate) {\n        this.rootComponent = components.get();\n        this.visibilityPredicate = predicate;\n\n        this.rootComponent.inflate(Size.of(this.virtualWidth, this.virtualHeight));\n        this.rootComponent.mount(null, 0, 0);\n    }\n\n    protected BaseOwoToast(Supplier<R> rootComponent, Duration timeout) {\n        this(rootComponent, VisibilityPredicate.timeout(timeout));\n    }\n\n    private Visibility visibility = Visibility.HIDE;\n\n    @Override\n    public void update(ToastManager manager, long time) {\n        final var delta = Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaTicks();\n\n        var client = manager.getMinecraft();\n        var window = client.getWindow();\n\n        int mouseX = -1000; //(int)(client.mouse.getX() * (double) window.getScaledWidth() / (double) window.getWidth());\n        int mouseY = -1000; //(int)(client.mouse.getY() * (double) window.getScaledHeight() / (double) window.getHeight());\n\n        this.rootComponent.update(delta, mouseX, mouseY);\n\n        this.visibility = this.visibilityPredicate.test(this, time);\n    }\n\n    @Override\n    public Visibility getWantedVisibility() {\n        return this.visibility;\n    }\n\n    @Override\n    public void render(GuiGraphics context, Font textRenderer, long startTime) {\n        var tickCounter = Minecraft.getInstance().getDeltaTracker();\n\n        this.rootComponent.draw(OwoUIGraphics.of(context), -1000, -1000, tickCounter.getGameTimeDeltaPartialTick(false), tickCounter.getGameTimeDeltaTicks());\n    }\n\n    @Override\n    public int height() {\n        return this.rootComponent.fullSize().height();\n    }\n\n    @Override\n    public int width() {\n        return this.rootComponent.fullSize().width();\n    }\n\n    @FunctionalInterface\n    public interface VisibilityPredicate<R extends ParentUIComponent> {\n        Visibility test(BaseOwoToast<R> toast, long startTime);\n\n        static <R extends ParentUIComponent> VisibilityPredicate<R> timeout(Duration timeout) {\n            return (toast, startTime) -> System.currentTimeMillis() - startTime <= timeout.get(ChronoUnit.MILLIS) ? Visibility.HIDE : Visibility.SHOW;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseOwoTooltipComponent.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Size;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.function.Supplier;\n\n@ApiStatus.Experimental\npublic abstract class BaseOwoTooltipComponent<R extends ParentUIComponent> implements ClientTooltipComponent {\n\n    protected final R rootComponent;\n    protected int virtualWidth = 1000, virtualHeight = 1000;\n\n    protected BaseOwoTooltipComponent(Supplier<R> components) {\n        this.rootComponent = components.get();\n\n        this.rootComponent.inflate(Size.of(this.virtualWidth, this.virtualHeight));\n        this.rootComponent.mount(null, 0, 0);\n    }\n\n    @Override\n    public void renderImage(Font textRenderer, int x, int y, int width, int height, GuiGraphics context) {\n        var tickCounter = Minecraft.getInstance().getDeltaTracker();\n\n        this.rootComponent.moveTo(x, y);\n        this.rootComponent.draw(OwoUIGraphics.of(context), -1000, -1000, tickCounter.getGameTimeDeltaPartialTick(false), tickCounter.getGameTimeDeltaTicks());\n    }\n\n    @Override\n    public int getHeight(Font textRenderer) {\n        return this.rootComponent.fullSize().height();\n    }\n\n    @Override\n    public int getWidth(Font textRenderer) {\n        return this.rootComponent.fullSize().width();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseParentUIComponent.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.ui.container.WrappingParentUIComponent;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.util.FocusHandler;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * The reference implementation of the {@link ParentUIComponent} interface,\n * serving as a base for all parent components on owo-ui. If you need your own parent\n * component, it is often beneficial to subclass one of owo-ui's existing layout classes,\n * especially {@link WrappingParentUIComponent} is often useful\n */\npublic abstract class BaseParentUIComponent extends BaseUIComponent implements ParentUIComponent {\n\n    protected final Observable<VerticalAlignment> verticalAlignment = Observable.of(VerticalAlignment.TOP);\n    protected final Observable<HorizontalAlignment> horizontalAlignment = Observable.of(HorizontalAlignment.LEFT);\n\n    protected final AnimatableProperty<Insets> padding = AnimatableProperty.of(Insets.none());\n\n    protected @Nullable FocusHandler focusHandler = null;\n    protected @Nullable ArrayList<Runnable> taskQueue = null;\n\n    protected Surface surface = Surface.BLANK;\n    protected boolean allowOverflow = false;\n\n    protected BaseParentUIComponent(Sizing horizontalSizing, Sizing verticalSizing) {\n        this.horizontalSizing.set(horizontalSizing);\n        this.verticalSizing.set(verticalSizing);\n\n        Observable.observeAll(this::updateLayout, horizontalAlignment, verticalAlignment, padding);\n    }\n\n    @Override\n    public final void update(float delta, int mouseX, int mouseY) {\n        ParentUIComponent.super.update(delta, mouseX, mouseY);\n        super.update(delta, mouseX, mouseY);\n        this.parentUpdate(delta, mouseX, mouseY);\n\n        if (this.taskQueue != null) {\n            this.taskQueue.forEach(Runnable::run);\n            this.taskQueue.clear();\n        }\n    }\n\n    /**\n     * Update the state of this component before drawing\n     * the next frame. This method is separated from\n     * {@link #update(float, int, int)} to enforce the task\n     * queue always being run last\n     *\n     * @param delta  The duration of the last frame, in partial ticks\n     * @param mouseX The mouse pointer's x-coordinate\n     * @param mouseY The mouse pointer's y-coordinate\n     */\n    protected void parentUpdate(float delta, int mouseX, int mouseY) {}\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        this.surface.draw(graphics, this);\n    }\n\n    @Override\n    public void queue(Runnable task) {\n        if (this.taskQueue == null) {\n            this.parent.queue(task);\n        } else {\n            this.taskQueue.add(task);\n        }\n    }\n\n    @Override\n    public @Nullable FocusHandler focusHandler() {\n        if (this.focusHandler == null) {\n            return super.focusHandler();\n        } else {\n            return this.focusHandler;\n        }\n    }\n\n    @Override\n    public ParentUIComponent verticalAlignment(VerticalAlignment alignment) {\n        this.verticalAlignment.set(alignment);\n        return this;\n    }\n\n    @Override\n    public VerticalAlignment verticalAlignment() {\n        return this.verticalAlignment.get();\n    }\n\n    @Override\n    public ParentUIComponent horizontalAlignment(HorizontalAlignment alignment) {\n        this.horizontalAlignment.set(alignment);\n        return this;\n    }\n\n    @Override\n    public HorizontalAlignment horizontalAlignment() {\n        return this.horizontalAlignment.get();\n    }\n\n    @Override\n    public ParentUIComponent padding(Insets padding) {\n        this.padding.set(padding);\n        this.updateLayout();\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Insets> padding() {\n        return this.padding;\n    }\n\n    @Override\n    public ParentUIComponent allowOverflow(boolean allowOverflow) {\n        this.allowOverflow = allowOverflow;\n        return this;\n    }\n\n    @Override\n    public boolean allowOverflow() {\n        return this.allowOverflow;\n    }\n\n    @Override\n    public ParentUIComponent surface(Surface surface) {\n        this.surface = surface;\n        return this;\n    }\n\n    @Override\n    public Surface surface() {\n        return this.surface;\n    }\n\n    @Override\n    public void mount(ParentUIComponent parent, int x, int y) {\n        super.mount(parent, x, y);\n        if (parent == null && this.focusHandler == null) {\n            this.focusHandler = new FocusHandler(this);\n            this.taskQueue = new ArrayList<>();\n        }\n    }\n\n    @Override\n    public void inflate(Size space) {\n        if (this.space.equals(space) && !this.dirty) return;\n        this.space = space;\n\n        for (var child : this.children()) {\n            child.dismount(DismountReason.LAYOUT_INFLATION);\n        }\n\n        super.inflate(space);\n        this.layout(space);\n        super.inflate(space);\n    }\n\n    protected void updateLayout() {\n        if (!this.mounted) return;\n\n        if (this.batchedEvents > 0) {\n            this.batchedEvents++;\n            return;\n        }\n\n        var previousSize = this.fullSize();\n\n        this.dirty = true;\n        this.inflate(this.space);\n\n        if (!previousSize.equals(this.fullSize()) && this.parent != null) {\n            this.parent.onChildMutated(this);\n        }\n    }\n\n    @Override\n    protected void runAndDeferEvents(Runnable action) {\n        try {\n            this.batchedEvents = 1;\n            action.run();\n        } finally {\n            if (this.batchedEvents > 1) {\n                this.batchedEvents = 0;\n                this.updateLayout();\n            } else {\n                this.batchedEvents = 0;\n            }\n        }\n    }\n\n    @Override\n    public void onChildMutated(UIComponent child) {\n        this.updateLayout();\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        if (this.focusHandler != null) {\n            this.focusHandler.updateClickFocus(this.x + click.x(), this.y + click.y());\n        }\n\n        return ParentUIComponent.super.onMouseDown(click, doubled)\n            || super.onMouseDown(click, doubled);\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        if (this.focusHandler != null && this.focusHandler.focused() != null) {\n            final var focused = this.focusHandler.focused();\n            return focused.onMouseUp(new MouseButtonEvent(this.x + click.x() - focused.x(), this.y + click.y() - focused.y(), click.buttonInfo()));\n        } else {\n            return super.onMouseUp(click);\n        }\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        return ParentUIComponent.super.onMouseScroll(mouseX, mouseY, amount) || super.onMouseScroll(mouseX, mouseY, amount);\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        if (this.focusHandler != null && this.focusHandler.focused() != null) {\n            final var focused = this.focusHandler.focused();\n            return focused.onMouseDrag(new MouseButtonEvent(this.x + click.x() - focused.x(), this.y + click.y() - focused.y(), click.buttonInfo()), deltaX, deltaY);\n        } else {\n            return super.onMouseDrag(click, deltaX, deltaY);\n        }\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        if (this.focusHandler == null) return false;\n\n        if (input.isCycleFocus()) {\n            this.focusHandler.cycle(!input.hasShiftDown());\n        } else if ((input.isUp() || input.isDown() || input.isLeft() || input.isRight()) && input.hasAltDown()) {\n            this.focusHandler.moveFocus(input.key());\n        } else if (this.focusHandler.focused() != null) {\n            return this.focusHandler.focused().onKeyPress(input);\n        }\n\n        return super.onKeyPress(input);\n    }\n\n    @Override\n    public boolean onCharTyped(CharacterEvent input) {\n        if (this.focusHandler == null) return false;\n\n        if (this.focusHandler.focused() != null) {\n            return this.focusHandler.focused().onCharTyped(input);\n        }\n\n        return super.onCharTyped(input);\n    }\n\n    @Override\n    public void updateX(int x) {\n        int offset = x - this.x;\n        super.updateX(x);\n\n        for (var child : this.children()) {\n            child.updateX(child.baseX() + offset);\n        }\n    }\n\n    @Override\n    public void updateY(int y) {\n        int offset = y - this.y;\n        super.updateY(y);\n\n        for (var child : this.children()) {\n            child.updateY(child.baseY() + offset);\n        }\n    }\n\n    /**\n     * @return The offset from the origin of this component\n     * at which children can start to be mounted. Accumulates\n     * padding as well as padding from content sizing\n     */\n    protected Size childMountingOffset() {\n        var padding = this.padding.get();\n        return Size.of(padding.left(), padding.top());\n    }\n\n    /**\n     * Mount a child using the given mounting function if its positioning\n     * is equal to {@link Positioning#layout()}, or according to its\n     * intrinsic positioning otherwise\n     *\n     * @param child      The child to mount\n     * @param layoutFunc The mounting function for components which follow the layout\n     */\n    protected void mountChild(@Nullable UIComponent child, Consumer<UIComponent> layoutFunc) {\n        if (child == null) return;\n\n        final var positioning = child.positioning().get();\n        final var componentMargins = child.margins().get();\n        final var padding = this.padding.get();\n\n        switch (positioning.type) {\n            case LAYOUT -> layoutFunc.accept(child);\n            case ABSOLUTE -> child.mount(\n                this,\n                this.x + positioning.x + componentMargins.left() + padding.left(),\n                this.y + positioning.y + componentMargins.top() + padding.top()\n            );\n            case RELATIVE -> child.mount(\n                this,\n                this.x + padding.left() + componentMargins.left() + Math.round((positioning.x / 100f) * (this.width() - child.fullSize().width() - padding.horizontal())),\n                this.y + padding.top() + componentMargins.top() + Math.round((positioning.y / 100f) * (this.height() - child.fullSize().height() - padding.vertical()))\n            );\n            case ACROSS -> child.mount(\n                this,\n                this.x + padding.left() + componentMargins.left() + Math.round((positioning.x / 100f) * (this.width() - padding.horizontal())),\n                this.y + padding.top() + componentMargins.top() + Math.round((positioning.y / 100f) * (this.height() - padding.vertical()))\n            );\n        }\n    }\n\n    /**\n     * Draw the children of this component along with\n     * their focus outline and tooltip, optionally clipping\n     * them if {@link #allowOverflow} is {@code false}\n     *\n     * @param children The list of children to draw\n     */\n    protected void drawChildren(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta, List<? extends UIComponent> children) {\n        if (!this.allowOverflow) {\n            var padding = this.padding.get();\n            context.enableScissor(this.x + padding.left(), this.y + padding.top(), this.x + padding.left() + this.width - padding.horizontal(), this.y + padding.top() + this.height - padding.vertical());\n        }\n\n        var focusHandler = this.focusHandler();\n        //noinspection ForLoopReplaceableByForEach\n        for (int i = 0; i < children.size(); i++) {\n            final var child = children.get(i);\n\n            if (!context.intersectsScissor(child)) continue;\n\n            child.draw(context, mouseX, mouseY, partialTicks, delta);\n            if (focusHandler.lastFocusSource() == FocusSource.KEYBOARD_CYCLE && focusHandler.focused() == child) {\n                child.drawFocusHighlight(context, mouseX, mouseY, partialTicks, delta);\n            }\n        }\n\n        if (!this.allowOverflow) {\n            context.disableScissor();\n        }\n    }\n\n    /**\n     * Calculate the space for child inflation. If a given axis\n     * is content-sized, return the respective value from {@code thisSpace}\n     *\n     * @param thisSpace The space for layout inflation of this widget\n     * @return The available space for child inflation\n     */\n    protected Size calculateChildSpace(Size thisSpace) {\n        final var padding = this.padding.get();\n\n        return Size.of(\n            Mth.lerpInt(this.horizontalSizing.get().contentFactor(), this.width - padding.horizontal(), thisSpace.width() - padding.horizontal()),\n            Mth.lerpInt(this.verticalSizing.get().contentFactor(), this.height - padding.vertical(), thisSpace.height() - padding.vertical())\n        );\n    }\n\n    @Override\n    public BaseParentUIComponent positioning(Positioning positioning) {\n        return (BaseParentUIComponent) super.positioning(positioning);\n    }\n\n    @Override\n    public BaseParentUIComponent margins(Insets margins) {\n        return (BaseParentUIComponent) super.margins(margins);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseUIComponent.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.event.*;\nimport io.wispforest.owo.ui.util.FocusHandler;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * The reference implementation of the {@link UIComponent} interface,\n * ideally you should extend this when making your own components\n */\npublic abstract class BaseUIComponent implements UIComponent {\n\n    @Nullable protected ParentUIComponent parent = null;\n    @Nullable protected String id = null;\n\n    protected boolean mounted = false;\n\n    protected int batchedEvents = 0;\n\n    protected final AnimatableProperty<Insets> margins = AnimatableProperty.of(Insets.none());\n\n    protected final AnimatableProperty<Positioning> positioning = AnimatableProperty.of(Positioning.layout());\n    protected final AnimatableProperty<Sizing> horizontalSizing = AnimatableProperty.of(Sizing.content());\n    protected final AnimatableProperty<Sizing> verticalSizing = AnimatableProperty.of(Sizing.content());\n\n    protected final EventStream<MouseDown> mouseDownEvents = MouseDown.newStream();\n    protected final EventStream<MouseUp> mouseUpEvents = MouseUp.newStream();\n    protected final EventStream<MouseScroll> mouseScrollEvents = MouseScroll.newStream();\n    protected final EventStream<MouseDrag> mouseDragEvents = MouseDrag.newStream();\n    protected final EventStream<KeyPress> keyPressEvents = KeyPress.newStream();\n    protected final EventStream<CharTyped> charTypedEvents = CharTyped.newStream();\n    protected final EventStream<FocusGained> focusGainedEvents = FocusGained.newStream();\n    protected final EventStream<FocusLost> focusLostEvents = FocusLost.newStream();\n\n    protected final EventStream<MouseEnter> mouseEnterEvents = MouseEnter.newStream();\n    protected final EventStream<MouseLeave> mouseLeaveEvents = MouseLeave.newStream();\n\n    protected boolean hovered = false;\n    protected boolean dirty = false;\n\n    protected CursorStyle cursorStyle = CursorStyle.NONE;\n    protected List<ClientTooltipComponent> tooltip = List.of();\n\n    protected int x, y;\n    protected int width, height;\n\n    protected Size space = Size.zero();\n\n    protected BaseUIComponent() {\n        Observable.observeAll(this::notifyParentIfMounted, margins, positioning, horizontalSizing, verticalSizing);\n    }\n\n    /**\n     * @return The horizontal size this component needs to fit its contents\n     */\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        throw new UnsupportedOperationException(this.getClass().getSimpleName() + \" does not support Sizing.content() on the horizontal axis\");\n    }\n\n    /**\n     * @return The vertical size this component needs to fit its contents\n     */\n    protected int determineVerticalContentSize(Sizing sizing) {\n        throw new UnsupportedOperationException(this.getClass().getSimpleName() + \" does not support Sizing.content() on the vertical axis\");\n    }\n\n    @Override\n    public void inflate(Size space) {\n        this.space = space;\n        this.applySizing();\n        this.dirty = false;\n    }\n\n    /**\n     * Calculate and apply the sizing of this component\n     * according to the last known expansion space\n     */\n    protected void applySizing() {\n        final var horizontalSizing = this.horizontalSizing.get();\n        final var verticalSizing = this.verticalSizing.get();\n\n        final var margins = this.margins.get();\n\n        this.width = horizontalSizing.inflate(this.space.width() - margins.horizontal(), this::determineHorizontalContentSize);\n        this.height = verticalSizing.inflate(this.space.height() - margins.vertical(), this::determineVerticalContentSize);\n    }\n\n    protected void notifyParentIfMounted() {\n        if (!this.hasParent()) return;\n\n        if (this.batchedEvents > 0) {\n            this.batchedEvents++;\n            return;\n        }\n\n        this.dirty = true;\n        this.parent.onChildMutated(this);\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public <C extends UIComponent> C configure(Consumer<C> closure) {\n        try {\n            this.runAndDeferEvents(() -> closure.accept((C) this));\n        } catch (ClassCastException theUserDidBadItWasNotMyFault) {\n            throw new IllegalArgumentException(\n                    \"Invalid target class passed when configuring component of type \" + this.getClass().getSimpleName(),\n                    theUserDidBadItWasNotMyFault\n            );\n        }\n\n        return (C) this;\n    }\n\n    protected void runAndDeferEvents(Runnable action) {\n        try {\n            this.batchedEvents = 1;\n            action.run();\n        } finally {\n            if (this.batchedEvents > 1) {\n                this.batchedEvents = 0;\n                this.notifyParentIfMounted();\n            } else {\n                this.batchedEvents = 0;\n            }\n        }\n    }\n\n    @Override\n    public void update(float delta, int mouseX, int mouseY) {\n        UIComponent.super.update(delta, mouseX, mouseY);\n\n        boolean nowHovered = this.isInBoundingBox(mouseX, mouseY);\n        if (this.hovered != nowHovered) {\n            this.updateHoveredState(mouseX, mouseY, nowHovered);\n        }\n    }\n\n    protected void updateHoveredState(int mouseX, int mouseY, boolean nowHovered) {\n        this.hovered = nowHovered;\n\n        if (nowHovered) {\n            if (this.root() == null || this.root().childAt(mouseX, mouseY) != this) {\n                this.hovered = false;\n                return;\n            }\n\n            this.mouseEnterEvents.sink().onMouseEnter();\n        } else {\n            this.mouseLeaveEvents.sink().onMouseLeave();\n        }\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        return this.mouseDownEvents.sink().onMouseDown(click, doubled);\n    }\n\n    @Override\n    public EventSource<MouseDown> mouseDown() {\n        return this.mouseDownEvents.source();\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        return this.mouseUpEvents.sink().onMouseUp(click);\n    }\n\n    @Override\n    public EventSource<MouseUp> mouseUp() {\n        return this.mouseUpEvents.source();\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        return this.mouseScrollEvents.sink().onMouseScroll(mouseX, mouseY, amount);\n    }\n\n    @Override\n    public EventSource<MouseScroll> mouseScroll() {\n        return this.mouseScrollEvents.source();\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.mouseDragEvents.sink().onMouseDrag(click, deltaX, deltaY);\n    }\n\n    @Override\n    public EventSource<MouseDrag> mouseDrag() {\n        return this.mouseDragEvents.source();\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        return this.keyPressEvents.sink().onKeyPress(input);\n    }\n\n    @Override\n    public EventSource<KeyPress> keyPress() {\n        return this.keyPressEvents.source();\n    }\n\n    @Override\n    public boolean onCharTyped(CharacterEvent input) {\n        return this.charTypedEvents.sink().onCharTyped(input);\n    }\n\n    @Override\n    public EventSource<CharTyped> charTyped() {\n        return this.charTypedEvents.source();\n    }\n\n    @Override\n    public void onFocusGained(FocusSource source) {\n        this.focusGainedEvents.sink().onFocusGained(source);\n    }\n\n    @Override\n    public EventSource<FocusGained> focusGained() {\n        return this.focusGainedEvents.source();\n    }\n\n    @Override\n    public void onFocusLost() {\n        this.focusLostEvents.sink().onFocusLost();\n    }\n\n    @Override\n    public EventSource<FocusLost> focusLost() {\n        return this.focusLostEvents.source();\n    }\n\n    @Override\n    public EventSource<MouseEnter> mouseEnter() {\n        return this.mouseEnterEvents.source();\n    }\n\n    @Override\n    public EventSource<MouseLeave> mouseLeave() {\n        return this.mouseLeaveEvents.source();\n    }\n\n    @Override\n    public CursorStyle cursorStyle() {\n        return this.cursorStyle;\n    }\n\n    @Override\n    public BaseUIComponent cursorStyle(CursorStyle style) {\n        this.cursorStyle = style;\n        return this;\n    }\n\n    @Override\n    public UIComponent tooltip(List<ClientTooltipComponent> tooltip) {\n        this.tooltip = tooltip;\n        return this;\n    }\n\n    @Override\n    public List<ClientTooltipComponent> tooltip() {\n        return this.tooltip;\n    }\n\n    @Override\n    public void mount(ParentUIComponent parent, int x, int y) {\n        this.parent = parent;\n        this.mounted = true;\n        this.moveTo(x, y);\n    }\n\n    @Override\n    public void dismount(DismountReason reason) {\n        this.parent = null;\n        this.mounted = false;\n    }\n\n    @Override\n    public ParentUIComponent parent() {\n        return this.parent;\n    }\n\n    @Override\n    public @Nullable FocusHandler focusHandler() {\n        return this.hasParent() ? this.parent.focusHandler() : null;\n    }\n\n    @Override\n    public BaseUIComponent positioning(Positioning positioning) {\n        this.positioning.set(positioning);\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Positioning> positioning() {\n        return this.positioning;\n    }\n\n    @Override\n    public BaseUIComponent margins(Insets margins) {\n        this.margins.set(margins);\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Insets> margins() {\n        return this.margins;\n    }\n\n    @Override\n    public UIComponent horizontalSizing(Sizing horizontalSizing) {\n        this.horizontalSizing.set(horizontalSizing);\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Sizing> horizontalSizing() {\n        return this.horizontalSizing;\n    }\n\n    @Override\n    public UIComponent verticalSizing(Sizing verticalSizing) {\n        this.verticalSizing.set(verticalSizing);\n        return this;\n    }\n\n    @Override\n    public AnimatableProperty<Sizing> verticalSizing() {\n        return this.verticalSizing;\n    }\n\n    @Override\n    public UIComponent id(@Nullable String id) {\n        this.id = id;\n        return this;\n    }\n\n    @Override\n    public @Nullable String id() {\n        return this.id;\n    }\n\n    @Override\n    public int x() {\n        return this.x;\n    }\n\n    @Override\n    public void updateX(int x) {\n        this.x = x;\n    }\n\n    @Override\n    public int y() {\n        return this.y;\n    }\n\n    @Override\n    public void updateY(int y) {\n        this.y = y;\n    }\n\n    @Override\n    public int width() {\n        return this.width;\n    }\n\n    @Override\n    public int height() {\n        return this.height;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseUIModelContainerScreen.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.core.OwoUIAdapter;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.parsing.ConfigureHotReloadScreen;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\npublic abstract class BaseUIModelContainerScreen<R extends ParentUIComponent, S extends AbstractContainerMenu> extends BaseOwoContainerScreen<R, S> {\n\n    /**\n     * The UI model this screen is built upon, parsed from XML.\n     * This is usually not relevant to subclasses, the UI adapter\n     * inherited from {@link BaseOwoScreen} is more interesting\n     */\n    protected final UIModel model;\n    protected final Class<R> rootComponentClass;\n\n    protected final @Nullable Identifier modelId;\n\n    protected BaseUIModelContainerScreen(S handler, Inventory inventory, Component title, Class<R> rootComponentClass, BaseUIModelScreen.DataSource source) {\n        super(handler, inventory, title);\n        var providedModel = source.get();\n        if (providedModel == null) {\n            source.reportError();\n            this.invalid = true;\n        }\n\n        this.rootComponentClass = rootComponentClass;\n        this.model = providedModel;\n\n        this.modelId = source instanceof BaseUIModelScreen.DataSource.AssetDataSource assetSource\n                ? assetSource.assetPath()\n                : null;\n    }\n\n    protected BaseUIModelContainerScreen(S handler, Inventory inventory, Component title, Class<R> rootComponentClass, Identifier modelId) {\n        this(handler, inventory, title, rootComponentClass, BaseUIModelScreen.DataSource.asset(modelId));\n    }\n\n    @Override\n    protected @NotNull OwoUIAdapter<R> createAdapter() {\n        return this.model.createAdapter(rootComponentClass, this);\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (Owo.DEBUG && this.modelId != null && input.key() == GLFW.GLFW_KEY_F5 && input.hasControlDown()) {\n            this.minecraft.setScreen(new ConfigureHotReloadScreen(this.modelId, this));\n            return true;\n        }\n\n        return super.keyPressed(input);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/base/BaseUIModelScreen.java",
    "content": "package io.wispforest.owo.ui.base;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.core.OwoUIAdapter;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.parsing.ConfigureHotReloadScreen;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIModelLoader;\nimport io.wispforest.owo.ui.util.UIErrorToast;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.nio.file.Path;\n\n/**\n * A simple base implementation of a screen that builds its UI\n * upon the base of a UI model parsed from an XML file. To work with this system,\n * declare your UI structure in an XML file and pass it into the super constructor\n * call using the relevant {@link DataSource}.\n * <p>\n * You can then query and set up different components of your UI hierarchy using\n * {@link ParentUIComponent#childById(Class, String)} in the {@link #build(ParentUIComponent)} method\n *\n * @param <R> The type of root component this screen expects from the UI model\n */\npublic abstract class BaseUIModelScreen<R extends ParentUIComponent> extends BaseOwoScreen<R> {\n\n    /**\n     * The UI model this screen is built upon, parsed from XML.\n     * This is usually not relevant to subclasses, the UI adapter\n     * inherited from {@link BaseOwoScreen} is more interesting\n     */\n    protected final UIModel model;\n    protected final Class<R> rootComponentClass;\n\n    protected final @Nullable Identifier modelId;\n\n    protected BaseUIModelScreen(Class<R> rootComponentClass, DataSource source) {\n        var providedModel = source.get();\n        if (providedModel == null) {\n            source.reportError();\n            this.invalid = true;\n        }\n\n        this.rootComponentClass = rootComponentClass;\n        this.model = providedModel;\n\n        this.modelId = source instanceof DataSource.AssetDataSource assetSource\n                ? assetSource.assetPath()\n                : null;\n    }\n\n    protected BaseUIModelScreen(Class<R> rootComponentClass, Identifier modelId) {\n        this(rootComponentClass, DataSource.asset(modelId));\n    }\n\n    @Override\n    protected @NotNull OwoUIAdapter<R> createAdapter() {\n        return this.model.createAdapter(this.rootComponentClass, this);\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (Owo.DEBUG && this.modelId != null && input.key() == GLFW.GLFW_KEY_F5 && input.hasControlDown()) {\n            this.minecraft.setScreen(new ConfigureHotReloadScreen(this.modelId, this));\n            return true;\n        }\n\n        return super.keyPressed(input);\n    }\n\n    /**\n     * A source of UI model data, by default can be loaded\n     * from a file or resourcepack. If you need a different way of\n     * fetching the model - implement this interface and pass it\n     * to the {@code super(...)} call in your constructor\n     */\n    public interface DataSource {\n\n        @Nullable\n        UIModel get();\n\n        void reportError();\n\n        /**\n         * Dynamically load the UI model by parsing the XML file\n         * at the given file path relative to the game's run directory.\n         * <p>\n         * This source is useful for development, as changes to the file\n         * instantly show up in-game without needing to reload resource packs\n         * <p>\n         * This source throws when running in release mode,\n         * because only files inside the jar can be shipped\n         *\n         * @param filePath The path of the XML file to load\n         * @deprecated Using the file data source directly is strongly discouraged\n         * as it primarily causes issues when accidentally used in production. Instead, use\n         * the asset source and configure the hot reload location for your model\n         */\n        @Deprecated\n        static DataSource file(String filePath) {\n            return new DataSource() {\n                @Override\n                @Nullable\n                public UIModel get() {\n                    if (!Owo.DEBUG) {\n                        throw new IllegalStateException(\"Debug UI data source must not be used in production\");\n                    }\n                    return UIModel.load(Path.of(filePath));\n                }\n\n                @Override\n                public void reportError() {\n                    UIErrorToast.report(\"Could not load UI model from file \" + filePath);\n                }\n            };\n        }\n\n        /**\n         * Get a statically loaded and parsed UI model from the currently\n         * loaded resource packs. This source is preferred as it has significantly\n         * higher performance due to completely avoiding I/O operations and the\n         * model XML can be overridden by different resource packs\n         *\n         * @param assetPath The path of the asset that was parsed into\n         *                  a UI model, relative to {@code assets/<namespace>/owo_ui}\n         */\n        static DataSource asset(Identifier assetPath) {\n            return new AssetDataSource(assetPath);\n        }\n\n        record AssetDataSource(Identifier assetPath) implements DataSource {\n            @Override\n            public @Nullable UIModel get() {\n                return UIModelLoader.get(assetPath);\n            }\n\n            @Override\n            public void reportError() {\n                UIErrorToast.report(\"No UI model with id \" + assetPath + \" was found\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/BlockComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.ui.access.BlockEntityAccessor;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.renderstate.BlockElementRenderState;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;\nimport net.minecraft.commands.arguments.blocks.BlockStateParser;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.storage.TagValueInput;\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\n\npublic class BlockComponent extends BaseUIComponent {\n\n    private final BlockState state;\n    private final @Nullable BlockEntity entity;\n\n    protected BlockComponent(BlockState state, @Nullable BlockEntity entity) {\n        this.state = state;\n        this.entity = entity;\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        BlockEntityRenderState entity = null;\n        if (this.entity != null) {\n            var renderer = Minecraft.getInstance().getBlockEntityRenderDispatcher().getRenderer(this.entity);\n            if (renderer != null) {\n                entity = renderer.createRenderState();\n\n                renderer.extractRenderState(\n                    this.entity, entity, partialTicks, Vec3.ZERO, null\n                );\n            }\n        }\n\n        graphics.guiRenderState.submitPicturesInPictureState(new BlockElementRenderState(\n            this.state,\n            entity,\n            new ScreenRectangle(this.x, this.y, this.width, this.height),\n            graphics.scissorStack.peek()\n        ));\n    }\n\n    protected static void prepareBlockEntity(BlockState state, BlockEntity blockEntity, @Nullable CompoundTag nbt) {\n        if (blockEntity == null) return;\n\n        var world = Minecraft.getInstance().level;\n\n        ((BlockEntityAccessor) blockEntity).owo$setBlockState(state);\n        blockEntity.setLevel(world);\n\n        if (nbt == null) return;\n\n        final var nbtCopy = nbt.copy();\n\n        nbtCopy.putInt(\"x\", 0);\n        nbtCopy.putInt(\"y\", 0);\n        nbtCopy.putInt(\"z\", 0);\n\n        blockEntity.loadWithComponents(TagValueInput.create(new ProblemReporter.ScopedCollector(Owo.LOGGER), world.registryAccess(), nbtCopy));\n    }\n\n    public static BlockComponent parse(Element element) {\n        UIParsing.expectAttributes(element, \"state\");\n\n        try {\n            var result = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, element.getAttribute(\"state\"), true);\n            return UIComponents.block(result.blockState(), result.nbt());\n        } catch (CommandSyntaxException cse) {\n            throw new UIModelParsingException(\"Invalid block state\", cse);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/BoxComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.AnimatableProperty;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\n\n/**\n * A colored rectangle either filled or outlined\n * by a given color or gradient\n */\npublic class BoxComponent extends BaseUIComponent {\n\n    protected boolean fill = false;\n    protected GradientDirection direction = GradientDirection.TOP_TO_BOTTOM;\n\n    protected final AnimatableProperty<Color> startColor = AnimatableProperty.of(Color.BLACK);\n    protected final AnimatableProperty<Color> endColor = AnimatableProperty.of(Color.BLACK);\n\n    public BoxComponent(Sizing horizontalSizing, Sizing verticalSizing) {\n        this.sizing(horizontalSizing, verticalSizing);\n    }\n\n    @Override\n    public void update(float delta, int mouseX, int mouseY) {\n        super.update(delta, mouseX, mouseY);\n        this.startColor.update(delta);\n        this.endColor.update(delta);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        final int startColor = this.startColor.get().argb();\n        final int endColor = this.endColor.get().argb();\n\n        if (this.fill) {\n            switch (this.direction) {\n                case TOP_TO_BOTTOM -> graphics.drawGradientRect(this.x, this.y, this.width, this.height,\n                        startColor, startColor, endColor, endColor);\n                case RIGHT_TO_LEFT -> graphics.drawGradientRect(this.x, this.y, this.width, this.height,\n                        endColor, startColor, startColor, endColor);\n                case BOTTOM_TO_TOP -> graphics.drawGradientRect(this.x, this.y, this.width, this.height,\n                        endColor, endColor, startColor, startColor);\n                case LEFT_TO_RIGHT -> graphics.drawGradientRect(this.x, this.y, this.width, this.height,\n                        startColor, endColor, endColor, startColor);\n            }\n        } else {\n            graphics.drawRectOutline(this.x, this.y, this.width, this.height, startColor);\n        }\n    }\n\n    /**\n     * Set whether this component should be\n     * filled with color or outlined\n     */\n    public BoxComponent fill(boolean fill) {\n        this.fill = fill;\n        return this;\n    }\n\n    /**\n     * @return {@code true} if this component is currently\n     * filled with color, {@code false} if it is outlined\n     */\n    public boolean fill() {\n        return this.fill;\n    }\n\n    /**\n     * Set the direction in which the gradient inside\n     * this component should travel\n     */\n    public BoxComponent direction(GradientDirection direction) {\n        this.direction = direction;\n        return this;\n    }\n\n    /**\n     * @return The direction in which the gradient inside\n     * this component currently travels\n     */\n    public GradientDirection direction() {\n        return this.direction;\n    }\n\n    /**\n     * Set the color of this component. Equivalent to calling\n     * both {@link #startColor(Color)} and {@link #endColor(Color)}\n     *\n     * @param color The start and end color of this\n     *              component's color gradient\n     */\n    public BoxComponent color(Color color) {\n        this.startColor.set(color);\n        this.endColor.set(color);\n        return this;\n    }\n\n    /**\n     * Set the start color of this component's gradient\n     */\n    public BoxComponent startColor(Color startColor) {\n        this.startColor.set(startColor);\n        return this;\n    }\n\n    /**\n     * @return The current start color of this component's gradient\n     */\n    public AnimatableProperty<Color> startColor() {\n        return this.startColor;\n    }\n\n    /**\n     * Set the end color of this component's gradient\n     */\n    public BoxComponent endColor(Color endColor) {\n        this.endColor.set(endColor);\n        return this;\n    }\n\n    /**\n     * @return The current end color of this component's gradient\n     */\n    public AnimatableProperty<Color> endColor() {\n        return this.endColor;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.expectChildren(element, children, \"sizing\");\n\n        UIParsing.apply(children, \"color\", Color::parse, this::color);\n        UIParsing.apply(children, \"start-color\", Color::parse, this::startColor);\n        UIParsing.apply(children, \"end-color\", Color::parse, this::endColor);\n        UIParsing.apply(children, \"fill\", UIParsing::parseBool, this::fill);\n        UIParsing.apply(children, \"direction\", UIParsing.parseEnum(GradientDirection.class), this::direction);\n    }\n\n    public enum GradientDirection {\n        TOP_TO_BOTTOM, /*TOP_LEFT_TO_BOTTOM_RIGHT,*/\n        RIGHT_TO_LEFT, /*TOP_RIGHT_TO_BOTTOM_LEFT,*/\n        BOTTOM_TO_TOP, /*BOTTOM_RIGHT_TO_TOP_LEFT,*/\n        LEFT_TO_RIGHT, /*BOTTOM_LEFT_TO_TOP_RIGHT*/\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/BraidComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.core.EventBinding;\nimport io.wispforest.owo.braid.core.Surface;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.core.cursor.SystemCursorStyle;\nimport io.wispforest.owo.braid.core.events.*;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.drag.DragArena;\nimport io.wispforest.owo.braid.widgets.drag.DragArenaElement;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Size;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.lang.ref.Cleaner;\nimport java.lang.ref.WeakReference;\nimport java.util.function.Consumer;\n\npublic class BraidComponent extends BaseUIComponent {\n\n    private static final Cleaner APP_CLEANER = Cleaner.create();\n\n    private final AppState appState;\n    private final EventBinding eventBinding = new EventBinding.Default();\n\n    private BraidWidget.State braidWidgetState;\n\n    private CursorStyle cursorStyle = CursorStyle.NONE;\n\n    public BraidComponent(Widget braidWidget) {\n        this.appState = new AppState(\n            null,\n            AppState.formatName(\"BraidComponent\", braidWidget),\n            Minecraft.getInstance(),\n            new EmbedSurface(this),\n            eventBinding,\n            new BraidWidget(\n                state -> braidWidgetState = state,\n                braidWidget\n            )\n        );\n\n        APP_CLEANER.register(this, new AppCleanCallback(appState));\n    }\n\n    @Override\n    public void inflate(Size space) {\n        super.inflate(space);\n        braidWidgetState.setState(() -> {\n            braidWidgetState.width = this.width;\n            braidWidgetState.height = this.height;\n        });\n    }\n\n    @Override\n    public void updateX(int x) {\n        super.updateX(x);\n        braidWidgetState.setState(() -> braidWidgetState.x = x);\n    }\n\n    @Override\n    public void updateY(int y) {\n        super.updateY(y);\n        braidWidgetState.setState(() -> braidWidgetState.y = y);\n    }\n\n    @Override\n    public void update(float delta, int mouseX, int mouseY) {\n        super.update(delta, mouseX, mouseY);\n\n        eventBinding.add(new MouseMoveEvent(mouseX, mouseY));\n        appState.processEvents(\n            delta\n        );\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        appState.draw(graphics);\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        eventBinding.add(new MouseButtonPressEvent(click.button(), click.modifiers()));\n        return true;\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        eventBinding.add(new MouseButtonReleaseEvent(click.button(), click.modifiers()));\n        return true;\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        // TODO: reconsider\n        eventBinding.add(new MouseScrollEvent(0, amount));\n        return true;\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        this.eventBinding.add(new KeyPressEvent(input.key(), input.scancode(), input.modifiers()));\n        this.eventBinding.add(new KeyReleaseEvent(input.key(), input.scancode(), input.modifiers()));\n        return true;\n    }\n\n    @Override\n    public boolean onCharTyped(CharacterEvent input) {\n        this.eventBinding.add(new CharInputEvent((char) input.codepoint(), input.modifiers()));\n        return true;\n    }\n\n    @Override\n    public io.wispforest.owo.ui.core.CursorStyle cursorStyle() {\n        if (!(cursorStyle instanceof SystemCursorStyle system)) return io.wispforest.owo.ui.core.CursorStyle.NONE;\n\n        return switch (system.glfwId) {\n            case GLFW.GLFW_ARROW_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.POINTER;\n            case GLFW.GLFW_IBEAM_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.TEXT;\n            case GLFW.GLFW_HAND_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.HAND;\n            case GLFW.GLFW_RESIZE_ALL_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.MOVE;\n            case GLFW.GLFW_CROSSHAIR_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.CROSSHAIR;\n            case GLFW.GLFW_HRESIZE_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.HORIZONTAL_RESIZE;\n            case GLFW.GLFW_VRESIZE_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.VERTICAL_RESIZE;\n            case GLFW.GLFW_RESIZE_NWSE_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.NWSE_RESIZE;\n            case GLFW.GLFW_RESIZE_NESW_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.NESW_RESIZE;\n            case GLFW.GLFW_NOT_ALLOWED_CURSOR -> io.wispforest.owo.ui.core.CursorStyle.NOT_ALLOWED;\n\n            default -> io.wispforest.owo.ui.core.CursorStyle.NONE;\n        };\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return true;\n    }\n\n    private record AppCleanCallback(AppState app) implements Runnable {\n        @Override\n        public void run() {\n            this.app.dispose();\n        }\n    }\n\n    public static class EmbedSurface extends Surface.Default {\n        // this is a weak reference so that the AppState can get properly collected\n        private final WeakReference<BraidComponent> parent;\n\n        public EmbedSurface(BraidComponent parent) {\n            this.parent = new WeakReference<>(parent);\n        }\n\n        @Override\n        public CursorStyle currentCursorStyle() {\n            //noinspection DataFlowIssue\n            return parent.get().cursorStyle;\n        }\n\n        @Override\n        public void setCursorStyle(CursorStyle style) {\n            //noinspection DataFlowIssue\n            parent.get().cursorStyle = style;\n        }\n    }\n\n    public static class BraidWidget extends StatefulWidget {\n\n        public Consumer<State> stateConsumer;\n\n        public final Widget child;\n\n        public BraidWidget(Consumer<State> stateConsumer, Widget child) {\n            this.stateConsumer = stateConsumer;\n            this.child = child;\n        }\n\n        @Override\n        public WidgetState<BraidWidget> createState() {\n            var state = new State();\n\n            this.stateConsumer.accept(state);\n            this.stateConsumer = null;\n\n            return state;\n        }\n\n        public static class State extends WidgetState<BraidWidget> {\n            private int x, y, width, height;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new DragArena(\n                    new DragArenaElement(\n                        x, y,\n                        new Sized(\n                            width, height,\n                            this.widget().child\n                        )\n                    )\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/ButtonComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.ui.access.AbstractWidgetAccessor;\nimport io.wispforest.owo.mixin.ui.access.ButtonAccessor;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic class ButtonComponent extends Button {\n\n    public static final Identifier ACTIVE_TEXTURE = Owo.id(\"button/active\");\n    public static final Identifier HOVERED_TEXTURE = Owo.id(\"button/hovered\");\n    public static final Identifier DISABLED_TEXTURE = Owo.id(\"button/disabled\");\n\n    protected Renderer renderer = Renderer.VANILLA;\n    protected boolean textShadow = true;\n\n    protected ButtonComponent(Component message, Consumer<ButtonComponent> onPress) {\n        super(0, 0, 0, 0, message, button -> onPress.accept((ButtonComponent) button), Button.DEFAULT_NARRATION);\n        this.sizing(Sizing.content());\n    }\n\n    @Override\n    public void renderContents(GuiGraphics context, int mouseX, int mouseY, float delta) {\n        this.renderer.draw((OwoUIGraphics) context, this, delta);\n\n        var textRenderer = Minecraft.getInstance().font;\n        int color = this.active ? 0xffffffff : 0xffa0a0a0;\n\n        if (this.textShadow) {\n            context.drawCenteredString(textRenderer, this.getMessage(), this.getX() + this.width / 2, this.getY() + (this.height - 8) / 2, color);\n        } else {\n            context.drawString(textRenderer, this.getMessage(), (int) (this.getX() + this.width / 2f - textRenderer.width(this.getMessage()) / 2f), (int) (this.getY() + (this.height - 8) / 2f), color, false);\n        }\n\n        var tooltip = ((AbstractWidgetAccessor) this).owo$getTooltip();\n        if (this.isHovered && tooltip.get() != null)\n            context.setTooltipForNextFrame(textRenderer, tooltip.get().toCharSequence(Minecraft.getInstance()), DefaultTooltipPositioner.INSTANCE, mouseX, mouseY, false);\n    }\n\n    public ButtonComponent onPress(Consumer<ButtonComponent> onPress) {\n        ((ButtonAccessor) this).owo$setOnPress(button -> onPress.accept((ButtonComponent) button));\n        return this;\n    }\n\n    public ButtonComponent renderer(Renderer renderer) {\n        this.renderer = renderer;\n        return this;\n    }\n\n    public Renderer renderer() {\n        return this.renderer;\n    }\n\n    public ButtonComponent textShadow(boolean textShadow) {\n        this.textShadow = textShadow;\n        return this;\n    }\n\n    public boolean textShadow() {\n        return this.textShadow;\n    }\n\n    public ButtonComponent active(boolean active) {\n        this.active = active;\n        return this;\n    }\n\n    public boolean active() {\n        return this.active;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"text\", UIParsing::parseText, this::setMessage);\n        UIParsing.apply(children, \"text-shadow\", UIParsing::parseBool, this::textShadow);\n        UIParsing.apply(children, \"renderer\", Renderer::parse, this::renderer);\n    }\n\n    protected CursorStyle owo$preferredCursorStyle() {\n        return CursorStyle.HAND;\n    }\n\n    @FunctionalInterface\n    public interface Renderer {\n        Renderer VANILLA = (matrices, button, delta) -> {\n            var texture = button.active\n                    ? button.isHovered ? HOVERED_TEXTURE : ACTIVE_TEXTURE\n                    : DISABLED_TEXTURE;\n            NinePatchTexture.draw(texture, matrices, button.getX(), button.getY(), button.width, button.height);\n        };\n\n        static Renderer flat(int color, int hoveredColor, int disabledColor) {\n            return (context, button, delta) -> {\n                if (button.active) {\n                    if (button.isHovered) {\n                        context.fill(button.getX(), button.getY(), button.getX() + button.width, button.getY() + button.height, hoveredColor);\n                    } else {\n                        context.fill(button.getX(), button.getY(), button.getX() + button.width, button.getY() + button.height, color);\n                    }\n                } else {\n                    context.fill(button.getX(), button.getY(), button.getX() + button.width, button.getY() + button.height, disabledColor);\n                }\n            };\n        }\n\n        static Renderer texture(Identifier texture, int u, int v, int textureWidth, int textureHeight) {\n            return (context, button, delta) -> {\n                int renderV = v;\n                if (!button.active) {\n                    renderV += button.height * 2;\n                } else if (button.isHovered()) {\n                    renderV += button.height;\n                }\n\n                context.blit(RenderPipelines.GUI_TEXTURED, texture, button.getX(), button.getY(), u, renderV, button.width, button.height, textureWidth, textureHeight);\n            };\n        }\n\n        void draw(OwoUIGraphics context, ButtonComponent button, float delta);\n\n        static Renderer parse(Element element) {\n            var children = UIParsing.<Element>allChildrenOfType(element, Node.ELEMENT_NODE);\n            if (children.size() > 1)\n                throw new UIModelParsingException(\"'renderer' declaration may only contain a single child\");\n\n            var rendererElement = children.get(0);\n            return switch (rendererElement.getNodeName()) {\n                case \"vanilla\" -> VANILLA;\n                case \"flat\" -> {\n                    UIParsing.expectAttributes(rendererElement, \"color\", \"hovered-color\", \"disabled-color\");\n                    yield flat(\n                            Color.parseAndPack(rendererElement.getAttributeNode(\"color\")),\n                            Color.parseAndPack(rendererElement.getAttributeNode(\"hovered-color\")),\n                            Color.parseAndPack(rendererElement.getAttributeNode(\"disabled-color\"))\n                    );\n                }\n                case \"texture\" -> {\n                    UIParsing.expectAttributes(rendererElement, \"texture\", \"u\", \"v\", \"texture-width\", \"texture-height\");\n                    yield texture(\n                            UIParsing.parseIdentifier(rendererElement.getAttributeNode(\"texture\")),\n                            UIParsing.parseUnsignedInt(rendererElement.getAttributeNode(\"u\")),\n                            UIParsing.parseUnsignedInt(rendererElement.getAttributeNode(\"v\")),\n                            UIParsing.parseUnsignedInt(rendererElement.getAttributeNode(\"texture-width\")),\n                            UIParsing.parseUnsignedInt(rendererElement.getAttributeNode(\"texture-height\"))\n                    );\n                }\n                default ->\n                        throw new UIModelParsingException(\"Unknown button renderer '\" + rendererElement.getNodeName() + \"'\");\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/CheckboxComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.mixin.ui.access.CheckboxAccessor;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.Size;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Checkbox;\nimport net.minecraft.client.input.InputWithModifiers;\nimport net.minecraft.network.chat.Component;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic class CheckboxComponent extends Checkbox {\n\n    protected final Observable<Boolean> listeners;\n\n    protected CheckboxComponent(Component message) {\n        super(0, 0, 0, message, Minecraft.getInstance().font, false, (checkbox, checked) -> {});\n        this.listeners = Observable.of(this.selected());\n        this.sizing(Sizing.content(), Sizing.fixed(20));\n    }\n\n    @Override\n    public void onPress(InputWithModifiers input) {\n        super.onPress(input);\n        this.listeners.set(this.selected());\n    }\n\n    public CheckboxComponent checked(boolean checked) {\n        ((CheckboxAccessor) this).owo$setSelected(checked);\n        this.listeners.set(this.selected());\n        return this;\n    }\n\n    public CheckboxComponent onChanged(Consumer<Boolean> listener) {\n        this.listeners.observe(listener);\n        return this;\n    }\n\n    @Override\n    public void inflate(Size space) {\n        super.inflate(space);\n        ((CheckboxAccessor) this).owo$getTextWidget().setMaxWidth(this.width);\n    }\n\n    @Override\n    public void setMessage(Component message) {\n        super.setMessage(message);\n        ((CheckboxAccessor)this).owo$getTextWidget().setMessage(message);\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"checked\", UIParsing::parseBool, this::checked);\n        UIParsing.apply(children, \"text\", UIParsing::parseText, this::setMessage);\n    }\n\n    public CursorStyle owo$preferredCursorStyle() {\n        return CursorStyle.HAND;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/ColorPickerComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.OwoUIPipelines;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.renderstate.GradientQuadElementRenderState;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.gui.navigation.ScreenPosition;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\n\npublic class ColorPickerComponent extends BaseUIComponent {\n\n    protected EventStream<OnChanged> changedEvents = OnChanged.newStream();\n    protected Observable<Color> selectedColor = Observable.of(Color.BLACK);\n\n    protected @Nullable Section lastClicked = null;\n\n    protected float hue = .5f;\n    protected float saturation = 1f;\n    protected float value = 1f;\n    protected float alpha = 1f;\n\n    protected int selectorWidth = 20;\n    protected int selectorPadding = 10;\n    protected boolean showAlpha = false;\n\n    // not exactly an ideal solution for location-sensitive cursor\n    // styles but the framework doesn't really let us do much\n    // better currently\n    //\n    // glisco, 20.05.2024\n    private int lastCursorX;\n\n    public ColorPickerComponent() {\n        this.selectedColor.observe(changedEvents.sink()::onChanged);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        this.lastCursorX = mouseX - this.x;\n\n        // Color area\n\n        graphics.guiRenderState.submitGuiElement(new GradientQuadElementRenderState(\n            OwoUIPipelines.GUI_HSV,\n            new Matrix3x2f(graphics.pose()),\n            new ScreenRectangle(new ScreenPosition(this.renderX(), this.renderY()), this.colorAreaWidth(), this.renderHeight()),\n            graphics.scissorStack.peek(),\n            new Color(this.hue, 0f, 1f),\n            new Color(this.hue, 1f, 1f),\n            new Color(this.hue, 0f, 0f),\n            new Color(this.hue, 1f, 0f)\n        ));\n\n        graphics.drawRectOutline(\n                (int) (this.renderX() + (this.saturation * this.colorAreaWidth()) - 1),\n                (int) (this.renderY() + ((1 - this.value) * (this.renderHeight() - 1)) - 1),\n                3, 3,\n                Color.WHITE.argb()\n        );\n\n        // Hue selector\n\n        graphics.drawSpectrum(this.renderX() + this.hueSelectorX(), this.renderY(), this.selectorWidth, this.renderHeight(), true);\n        graphics.drawRectOutline(\n                this.renderX() + this.hueSelectorX() - 1,\n                this.renderY() + (int) ((this.renderHeight() - 1) * (1 - this.hue) - 1),\n                this.selectorWidth + 2, 3,\n                Color.WHITE.argb()\n        );\n\n        // Alpha selector\n\n        if (this.showAlpha) {\n            var color = 0xFF << 24 | this.selectedColor.get().rgb();\n            graphics.drawGradientRect(this.renderX() + this.alphaSelectorX(), this.renderY(), this.selectorWidth, this.renderHeight(), color, color, 0, 0);\n            graphics.drawRectOutline(\n                    this.renderX() + this.alphaSelectorX() - 1,\n                    this.renderY() + (int) ((this.renderHeight() - 1) * (1 - this.alpha) - 1),\n                    this.selectorWidth + 2, 3,\n                    Color.WHITE.argb()\n            );\n        }\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        this.lastClicked = this.showAlpha && click.x() >= this.alphaSelectorX()\n                ? Section.ALPHA_SELECTOR\n                : click.x() > this.hueSelectorX()\n                ? Section.HUE_SELECTOR\n                : Section.COLOR_AREA;\n\n        this.updateFromMouse(click.x(), click.y());\n\n        super.onMouseDown(click, doubled);\n        return true;\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        this.updateFromMouse(click.x(), click.y());\n\n        super.onMouseDrag(click, deltaX, deltaY);\n        return true;\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return true;\n    }\n\n    @Override\n    public CursorStyle cursorStyle() {\n        var inColorArea = this.lastCursorX >= 0 && this.lastCursorX <= this.colorAreaWidth();\n        var inHueSelector = this.lastCursorX >= this.hueSelectorX() && this.lastCursorX <= this.hueSelectorX() + this.selectorWidth;\n        var inAlphaSelector = this.showAlpha && this.lastCursorX >= this.alphaSelectorX() && this.lastCursorX <= this.alphaSelectorX() + this.selectorWidth;\n\n        return inColorArea || inHueSelector || inAlphaSelector ? CursorStyle.MOVE : super.cursorStyle();\n    }\n\n    protected void updateFromMouse(double mouseX, double mouseY) {\n        mouseX = Mth.clamp(mouseX - 1, 0, this.renderWidth());\n        mouseY = Mth.clamp(mouseY - 1, 0, this.renderHeight());\n\n        if (this.lastClicked == Section.ALPHA_SELECTOR) {\n            this.alpha = 1f - (float) (mouseY / this.renderHeight());\n        } else if (this.lastClicked == Section.HUE_SELECTOR) {\n            this.hue = 1f - (float) (mouseY / this.renderHeight());\n        } else if (this.lastClicked == Section.COLOR_AREA) {\n            this.saturation = Math.min(1f, (float) (mouseX / this.colorAreaWidth()));\n            this.value = 1f - (float) (mouseY / this.renderHeight());\n        }\n\n        this.selectedColor.set(Color.ofHsv(this.hue, this.saturation, this.value, this.alpha));\n    }\n\n    protected int renderX() {\n        return this.x + 1;\n    }\n\n    protected int renderY() {\n        return this.y + 1;\n    }\n\n    protected int renderWidth() {\n        return this.width - 2;\n    }\n\n    protected int renderHeight() {\n        return this.height - 2;\n    }\n\n    protected int colorAreaWidth() {\n        return this.showAlpha\n                ? this.renderWidth() - this.selectorPadding - this.selectorWidth - this.selectorPadding - this.selectorWidth\n                : this.renderWidth() - this.selectorPadding - this.selectorWidth;\n    }\n\n    protected int hueSelectorX() {\n        return this.showAlpha\n                ? this.renderWidth() - this.selectorWidth - this.selectorPadding - this.selectorWidth\n                : this.renderWidth() - this.selectorWidth;\n    }\n\n    protected int alphaSelectorX() {\n        return this.renderWidth() - this.selectorWidth;\n    }\n\n    public ColorPickerComponent selectedColor(Color color) {\n        this.selectedColor.set(color);\n\n        var hsv = color.hsv();\n        this.hue = hsv[0];\n        this.saturation = hsv[1];\n        this.value = hsv[2];\n        this.alpha = color.alpha();\n\n        return this;\n    }\n\n    public ColorPickerComponent selectedColor(float hue, float saturation, float value) {\n        this.selectedColor.set(Color.ofHsv(hue, saturation, value));\n\n        this.hue = hue;\n        this.saturation = saturation;\n        this.value = value;\n        this.alpha = 1;\n\n        return this;\n    }\n\n    public Color selectedColor() {\n        return this.selectedColor.get();\n    }\n\n    public ColorPickerComponent selectorWidth(int selectorWidth) {\n        this.selectorWidth = selectorWidth;\n        return this;\n    }\n\n    public int selectorWidth() {\n        return selectorWidth;\n    }\n\n    public ColorPickerComponent selectorPadding(int selectorPadding) {\n        this.selectorPadding = selectorPadding;\n        return this;\n    }\n\n    public int selectorPadding() {\n        return selectorPadding;\n    }\n\n    public ColorPickerComponent showAlpha(boolean showAlpha) {\n        this.showAlpha = showAlpha;\n        return this;\n    }\n\n    public boolean showAlpha() {\n        return showAlpha;\n    }\n\n    public EventSource<OnChanged> onChanged() {\n        return this.changedEvents.source();\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"show-alpha\", UIParsing::parseBool, this::showAlpha);\n        UIParsing.apply(children, \"selector-width\", UIParsing::parseUnsignedInt, this::selectorWidth);\n        UIParsing.apply(children, \"selector-padding\", UIParsing::parseUnsignedInt, this::selectorPadding);\n        UIParsing.apply(children, \"selected-color\", Color::parse, this::selectedColor);\n    }\n\n    protected enum Section {\n        COLOR_AREA, HUE_SELECTOR, ALPHA_SELECTOR\n    }\n\n    public interface OnChanged {\n        void onChanged(Color color);\n\n        static EventStream<OnChanged> newStream() {\n            return new EventStream<>(subscribers -> value -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onChanged(value);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/DiscreteSliderComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport net.minecraft.network.chat.Component;\nimport org.w3c.dom.Element;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.Map;\n\npublic class DiscreteSliderComponent extends SliderComponent {\n\n    protected double min, max;\n\n    protected int decimalPlaces = 0;\n    protected boolean snap = false;\n\n    protected DiscreteSliderComponent(Sizing horizontalSizing, double min, double max) {\n        super(horizontalSizing);\n\n        this.min = min;\n        this.max = max;\n\n        this.updateMessage();\n        this.message(Component::literal);\n    }\n\n    @Override\n    protected void applyValue() {\n        this.changedEvents.sink().onChanged(this.discreteValue());\n    }\n\n    @Override\n    protected void updateMessage() {\n        this.setMessage(this.messageProvider.apply(String.format(\"%.\" + decimalPlaces + \"f\", this.discreteValue())));\n    }\n\n    public double discreteValue() {\n        return new BigDecimal(this.min + this.value * (this.max - this.min)).setScale(this.decimalPlaces, RoundingMode.HALF_UP).doubleValue();\n    }\n\n    public DiscreteSliderComponent setFromDiscreteValue(double discreteValue) {\n        this.value((discreteValue - min) / (max - min));\n        return this;\n    }\n\n    public DiscreteSliderComponent decimalPlaces(int decimalPlaces) {\n        this.decimalPlaces = decimalPlaces;\n        return this;\n    }\n\n    public int decimalPlaces() {\n        return this.decimalPlaces;\n    }\n\n    public double min() {\n        return this.min;\n    }\n\n    public double max() {\n        return this.max;\n    }\n\n    public DiscreteSliderComponent snap(boolean snap) {\n        this.snap = snap;\n        return this;\n    }\n\n    public boolean snap() {\n        return this.snap;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"decimal-places\", UIParsing::parseUnsignedInt, this::decimalPlaces);\n        UIParsing.apply(children, \"value\", UIParsing::parseDouble, this::setFromDiscreteValue);\n    }\n\n    public static DiscreteSliderComponent parse(Element element) {\n        UIParsing.expectAttributes(element, \"min\", \"max\");\n        return new DiscreteSliderComponent(\n                Sizing.content(),\n                UIParsing.parseDouble(element.getAttributeNode(\"min\")),\n                UIParsing.parseDouble(element.getAttributeNode(\"max\"))\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/DropdownComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.UISounds;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.apache.commons.lang3.mutable.MutableBoolean;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\npublic class DropdownComponent extends FlowLayout {\n\n    protected static final Identifier ICONS_TEXTURE = Owo.id(\"textures/gui/dropdown_icons.png\");\n    protected final FlowLayout entries;\n    protected boolean closeWhenNotHovered = false;\n\n    protected DropdownComponent(Sizing horizontalSizing) {\n        super(Sizing.content(), Sizing.content(), Algorithm.HORIZONTAL);\n\n        this.entries = UIContainers.verticalFlow(horizontalSizing, Sizing.content());\n        this.entries.padding(Insets.of(1));\n        this.entries.allowOverflow(true);\n        this.entries.surface(Surface.flat(0xC7000000).and(Surface.blur(3, 5)).and(Surface.outline(0xFF121212)));\n\n        this.child(this.entries);\n    }\n\n    /**\n     * Open a context menu at the given location in the given screen,\n     * adjusting the position if needed to avoid overflowing screen space\n     *\n     * @param screen        The screen on which to operate\n     * @param rootComponent The root component onto which to mount the dropdown\n     * @param mountFunction The mounting function to use\n     * @param mouseX        The x-coordinate at which to open the dropdown\n     * @param mouseY        The y-coordinate at which to open the dropdown\n     * @param builder       A function to add entries to the dropdown\n     */\n    public static <R extends ParentUIComponent> DropdownComponent openContextMenu(Screen screen, R rootComponent, BiConsumer<R, DropdownComponent> mountFunction, double mouseX, double mouseY, Consumer<DropdownComponent> builder) {\n        var dropdown = new DropdownComponent(Sizing.content());\n        builder.accept(dropdown);\n\n        mountFunction.accept(rootComponent, dropdown);\n\n        int xLocation = (int) mouseX - rootComponent.x();\n        int yLocation = (int) mouseY - rootComponent.y();\n\n        if (xLocation + dropdown.width() > screen.width) {\n            xLocation -= xLocation + dropdown.width() - screen.width;\n        }\n        if (yLocation + dropdown.height() > screen.height) {\n            yLocation -= yLocation + dropdown.height() - screen.height;\n        }\n\n        dropdown.positioning(Positioning.absolute(xLocation, yLocation));\n\n        var dismounted = new MutableBoolean(false);\n        ScreenMouseEvents.beforeMouseClick(screen).register((screen_, click) -> {\n            if (dismounted.isTrue() || dropdown.isInBoundingBox(click.x(), click.y())) return;\n\n            rootComponent.removeChild(dropdown);\n            dismounted.setTrue();\n        });\n\n        return dropdown;\n    }\n\n    @Override\n    public ParentUIComponent surface(Surface surface) {\n        this.entries.surface(surface);\n\n        return this;\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n        if (this.closeWhenNotHovered && !this.isInBoundingBox(mouseX, mouseY)) {\n            this.queue(() -> {\n                this.closeWhenNotHovered(false);\n                this.parent.removeChild(this);\n            });\n        }\n    }\n\n    @Override\n    public void layout(Size space) {\n        super.layout(space);\n\n        var entries = this.entries.children();\n        for (int i = 0; i < entries.size(); i++) {\n            var entry = entries.get(i);\n            if (!(entry instanceof ResizeableComponent sizeable)) continue;\n\n            sizeable.setWidth(this.entries.width() - this.entries.padding().get().horizontal() - entry.margins().get().horizontal());\n        }\n    }\n\n    public DropdownComponent divider() {\n        this.entries.child(new Divider());\n        return this;\n    }\n\n    public DropdownComponent text(Component text) {\n        this.entries.child(UIComponents.label(text).color(Color.ofFormatting(ChatFormatting.GRAY)).margins(Insets.of(2)));\n        return this;\n    }\n\n    public DropdownComponent button(Component text, Consumer<DropdownComponent> onClick) {\n        this.entries.child(new Button(this, text, onClick).margins(Insets.of(2)));\n        return this;\n    }\n\n    public DropdownComponent checkbox(Component text, boolean state, Consumer<Boolean> onClick) {\n        this.entries.child(new Checkbox(this, text, state, onClick).margins(Insets.of(2)));\n        return this;\n    }\n\n    public DropdownComponent nested(Component text, Sizing horizontalSizing, Consumer<DropdownComponent> builder) {\n        var nested = new DropdownComponent(horizontalSizing);\n        builder.accept(nested);\n        this.entries.child(new NestEntry(this, text, nested).margins(Insets.of(2)));\n        return this;\n    }\n\n    @Override\n    public FlowLayout removeChild(UIComponent child) {\n        if (child == this.entries) {\n            this.queue(() -> {\n                this.closeWhenNotHovered(false);\n                this.parent.removeChild(this);\n            });\n        }\n        return super.removeChild(child);\n    }\n\n    public DropdownComponent closeWhenNotHovered(boolean closeWhenNotHovered) {\n        this.closeWhenNotHovered = closeWhenNotHovered;\n        return this;\n    }\n\n    public boolean closeWhenNotHovered() {\n        return this.closeWhenNotHovered;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"entries\", Function.identity(), this::parseAndApplyEntries);\n        UIParsing.apply(children, \"close-when-not-hovered\", UIParsing::parseBool, this::closeWhenNotHovered);\n    }\n\n    protected void parseAndApplyEntries(Element container) {\n        for (var node : UIParsing.allChildrenOfType(container, Node.ELEMENT_NODE)) {\n            var entry = (Element) node;\n\n            switch (entry.getNodeName()) {\n                case \"divider\" -> this.divider();\n                case \"text\" -> this.text(UIParsing.parseText(entry));\n                case \"button\" -> {\n                    var children = UIParsing.childElements(entry);\n                    UIParsing.expectChildren(entry, children, \"text\");\n\n                    var text = UIParsing.parseText(children.get(\"text\"));\n                    this.button(text, dropdownComponent -> {\n                    });\n                }\n                case \"checkbox\" -> {\n                    var children = UIParsing.childElements(entry);\n                    UIParsing.expectChildren(entry, children, \"text\", \"checked\");\n\n                    var text = UIParsing.parseText(children.get(\"text\"));\n                    var checked = UIParsing.parseBool(children.get(\"checked\"));\n\n                    this.checkbox(text, checked, aBoolean -> {\n                    });\n                }\n                case \"nested\" -> {\n                    var text = entry.getAttribute(\"translate\").equals(\"true\")\n                            ? Component.translatable(entry.getAttribute(\"name\"))\n                            : Component.literal(entry.getAttribute(\"name\"));\n                    this.nested(text, Sizing.content(), dropdownComponent -> dropdownComponent.parseAndApplyEntries(entry));\n                }\n            }\n        }\n    }\n\n    protected static void drawIconFromTexture(OwoUIGraphics context, ParentUIComponent dropdown, int y, int u, int v) {\n        context.blit(RenderPipelines.GUI_TEXTURED, ICONS_TEXTURE,\n                dropdown.x() + dropdown.width() - dropdown.padding().get().right() - 10, y,\n                u, v,\n                9, 9,\n                32, 32\n        );\n    }\n\n    protected interface ResizeableComponent {\n        void setWidth(int width);\n    }\n\n    protected static class Divider extends BaseUIComponent implements ResizeableComponent {\n\n        public Divider() {\n            this.sizing(Sizing.fixed(1));\n        }\n\n        @Override\n        public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n            var margins = this.margins.get();\n            graphics.fill(\n                    this.x - margins.left(),\n                    this.y - margins.top(),\n                    this.x + this.width + margins.right(),\n                    this.y + this.height + margins.bottom(),\n                    0xFF121212\n            );\n        }\n\n        @Override\n        public void setWidth(int width) {\n            this.width = width;\n        }\n    }\n\n    protected static class NestEntry extends LabelComponent {\n\n        private final DropdownComponent child;\n\n        protected NestEntry(DropdownComponent parentDropdown, Component text, DropdownComponent child) {\n            super(text);\n            this.child = child;\n\n            this.mouseEnter().subscribe(() -> {\n                child.margins(Insets.top(this.y - parentDropdown.y));\n\n                parentDropdown.queue(() -> {\n                    parentDropdown.removeChild(child);\n                    parentDropdown.child(child);\n                });\n            });\n        }\n\n        @Override\n        public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n            super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n            drawIconFromTexture(graphics, this.parent, this.y, 0, 16);\n\n            this.child.closeWhenNotHovered(!PositionedRectangle.of(this.x, this.y, this.parent.width(), this.height).isInBoundingBox(mouseX, mouseY));\n        }\n\n        @Override\n        protected int determineHorizontalContentSize(Sizing sizing) {\n            return super.determineHorizontalContentSize(sizing) + 17;\n        }\n    }\n\n    protected static class Button extends LabelComponent implements ResizeableComponent {\n\n        protected final DropdownComponent parentDropdown;\n        protected Consumer<DropdownComponent> onClick;\n\n        protected Button(DropdownComponent parentDropdown, Component text, Consumer<DropdownComponent> onClick) {\n            super(text);\n            this.onClick = onClick;\n            this.parentDropdown = parentDropdown;\n\n            this.margins(Insets.vertical(1));\n            this.cursorStyle(CursorStyle.HAND);\n        }\n\n        public void setWidth(int width) {\n            this.width = width;\n        }\n\n        @Override\n        public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n            super.onMouseDown(click, doubled);\n\n            this.onClick.accept(this.parentDropdown);\n            this.playInteractionSound();\n\n            return true;\n        }\n\n        @Override\n        public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n            if (this.isInBoundingBox(mouseX, mouseY)) {\n                var margins = this.margins.get();\n                graphics.fill(\n                        this.x - margins.left(),\n                        this.y - margins.top(),\n                        this.x + this.width + margins.right(),\n                        this.y + this.height + margins.bottom(),\n                        0x44FFFFFF\n                );\n            }\n\n            super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n        }\n\n        protected void playInteractionSound() {\n            UISounds.playButtonSound();\n        }\n    }\n\n    protected static class Checkbox extends Button {\n\n        protected boolean state;\n\n        public Checkbox(DropdownComponent parentDropdown, Component text, boolean state, Consumer<Boolean> onClick) {\n            super(parentDropdown, text, dropdownComponent -> {\n            });\n\n            this.state = state;\n            this.onClick = dropdownComponent -> {\n                this.state = !this.state;\n                onClick.accept(this.state);\n            };\n        }\n\n        @Override\n        public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n            super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n            drawIconFromTexture(graphics, this.parent, this.y, this.state ? 16 : 0, 0);\n        }\n\n        @Override\n        protected int determineHorizontalContentSize(Sizing sizing) {\n            return super.determineHorizontalContentSize(sizing) + 17;\n        }\n\n        @Override\n        protected void playInteractionSound() {\n            UISounds.playInteractionSound();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/EntityComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport com.mojang.authlib.GameProfile;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport com.mojang.math.Axis;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.ui.access.EntityRendererAccessor;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.renderstate.EntityElementRenderState;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.multiplayer.ClientPacketListener;\nimport net.minecraft.client.multiplayer.CommonListenerCookie;\nimport net.minecraft.client.multiplayer.LevelLoadTracker;\nimport net.minecraft.client.multiplayer.PlayerInfo;\nimport net.minecraft.client.player.LocalPlayer;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.entity.EntityRenderDispatcher;\nimport net.minecraft.client.resources.DefaultPlayerSkin;\nimport net.minecraft.client.telemetry.TelemetryEventSender;\nimport net.minecraft.client.telemetry.WorldSessionTelemetryManager;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.TagParser;\nimport net.minecraft.network.protocol.PacketFlow;\nimport net.minecraft.server.ServerLinks;\nimport net.minecraft.util.ProblemReporter;\nimport net.minecraft.util.Util;\nimport net.minecraft.world.entity.*;\nimport net.minecraft.world.entity.player.Input;\nimport net.minecraft.world.entity.player.PlayerModelPart;\nimport net.minecraft.world.entity.player.PlayerSkin;\nimport net.minecraft.world.level.storage.TagValueInput;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix4f;\nimport org.lwjgl.glfw.GLFW;\nimport org.w3c.dom.Element;\n\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic class EntityComponent<E extends Entity> extends BaseUIComponent {\n\n    protected final EntityRenderDispatcher manager;\n    protected final MultiBufferSource.BufferSource entityBuffers;\n    protected final E entity;\n\n    protected float mouseRotation = 0;\n    protected float scale = 1;\n    protected boolean lookAtCursor = false;\n    protected boolean allowMouseRotation = false;\n    protected boolean scaleToFit = false;\n    protected boolean showNametag = false;\n    protected Consumer<Matrix4f> transform = matrixStack -> {};\n\n    protected EntityComponent(Sizing sizing, E entity) {\n        final var client = Minecraft.getInstance();\n        this.manager = client.getEntityRenderDispatcher();\n        this.entityBuffers = client.renderBuffers().bufferSource();\n\n        this.entity = entity;\n\n        this.sizing(sizing);\n    }\n\n    @SuppressWarnings(\"DataFlowIssue\")\n    protected EntityComponent(Sizing sizing, EntityType<E> type, @Nullable CompoundTag nbt) {\n        final var client = Minecraft.getInstance();\n        this.manager = client.getEntityRenderDispatcher();\n        this.entityBuffers = client.renderBuffers().bufferSource();\n\n        this.entity = type.create(client.level, EntitySpawnReason.BREEDING);\n        if (nbt != null) entity.load(TagValueInput.create(new ProblemReporter.ScopedCollector(Owo.LOGGER), client.level.registryAccess(), nbt));\n        entity.absSnapTo(client.player.getX(), client.player.getY(), client.player.getZ());\n\n        this.sizing(sizing);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        var matrix = new Matrix4f();\n        matrix.scale(75 * this.scale * this.width / 64f, -75 * this.scale * this.height / 64f, -75 * this.scale);\n\n        matrix.translate(0, entity.getBbHeight() / 2f, 0);\n\n        this.transform.accept(matrix);\n\n        if (this.lookAtCursor) {\n            float xRotation = (float) Math.toDegrees(Math.atan((mouseY - this.y - this.height / 2f) / 40f));\n            float yRotation = (float) Math.toDegrees(Math.atan((mouseX - this.x - this.width / 2f) / 40f));\n\n            if (this.entity instanceof LivingEntity living) {\n                living.yHeadRotO = -yRotation;\n            }\n\n            this.entity.yRotO = -yRotation;\n            this.entity.xRotO = xRotation * .65f;\n\n            // We make sure the xRotation never becomes 0, as the lighting otherwise becomes very unhappy\n            if (xRotation == 0) xRotation = .1f;\n            matrix.rotate(Axis.XP.rotationDegrees(xRotation * .15f));\n            matrix.rotate(Axis.YP.rotationDegrees(yRotation * .15f));\n        } else {\n            matrix.rotate(Axis.XP.rotationDegrees(35));\n            matrix.rotate(Axis.YP.rotationDegrees(-45 + this.mouseRotation));\n        }\n\n        var entityState = this.manager.extractEntity(this.entity, partialTicks);\n        var renderer = this.manager.getRenderer(this.entity);\n\n        if (showNametag) {\n            entityState.nameTag = ((EntityRendererAccessor) renderer).owo$getNameTag(entity);\n            entityState.nameTagAttachment = entity.getAttachments().getNullable(EntityAttachment.NAME_TAG, 0, entity.getYRot(partialTicks));\n        } else {\n            entityState.nameTag = null;\n            entityState.nameTagAttachment = null;\n        }\n\n        graphics.guiRenderState.submitPicturesInPictureState(new EntityElementRenderState(\n            entityState,\n            matrix,\n            new ScreenRectangle(this.x, this.y, this.width, this.height),\n            graphics.scissorStack.peek()\n        ));\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        if (this.allowMouseRotation && click.button() == GLFW.GLFW_MOUSE_BUTTON_LEFT) {\n            this.mouseRotation += deltaX;\n\n            super.onMouseDrag(click, deltaX, deltaY);\n            return true;\n        } else {\n            return super.onMouseDrag(click, deltaX, deltaY);\n        }\n    }\n\n    public E entity() {\n        return this.entity;\n    }\n\n    public EntityComponent<E> allowMouseRotation(boolean allowMouseRotation) {\n        this.allowMouseRotation = allowMouseRotation;\n        return this;\n    }\n\n    public boolean allowMouseRotation() {\n        return this.allowMouseRotation;\n    }\n\n    public EntityComponent<E> lookAtCursor(boolean lookAtCursor) {\n        this.lookAtCursor = lookAtCursor;\n        return this;\n    }\n\n    public boolean lookAtCursor() {\n        return this.lookAtCursor;\n    }\n\n    public EntityComponent<E> scale(float scale) {\n        this.scale = scale;\n        return this;\n    }\n\n    public float scale() {\n        return this.scale;\n    }\n\n    public EntityComponent<E> scaleToFit(boolean scaleToFit) {\n        this.scaleToFit = scaleToFit;\n\n        if (scaleToFit) {\n            float xScale = .5f / entity.getBbWidth();\n            float yScale = .5f / entity.getBbHeight();\n\n            this.scale(Math.min(xScale, yScale));\n        }\n\n        return this;\n    }\n\n    public boolean scaleToFit() {\n        return this.scaleToFit;\n    }\n\n    public EntityComponent<E> transform(Consumer<Matrix4f> transform) {\n        this.transform = transform;\n        return this;\n    }\n\n    public Consumer<Matrix4f> transform() {\n        return transform;\n    }\n\n    public EntityComponent<E> showNametag(boolean showNametag) {\n        this.showNametag = showNametag;\n        return this;\n    }\n\n    public boolean showNametag() {\n        return showNametag;\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return source == FocusSource.MOUSE_CLICK;\n    }\n\n    public static RenderablePlayerEntity createRenderablePlayer(GameProfile profile) {\n        return new RenderablePlayerEntity(profile);\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"scale\", UIParsing::parseFloat, this::scale);\n        UIParsing.apply(children, \"look-at-cursor\", UIParsing::parseBool, this::lookAtCursor);\n        UIParsing.apply(children, \"mouse-rotation\", UIParsing::parseBool, this::allowMouseRotation);\n        UIParsing.apply(children, \"scale-to-fit\", UIParsing::parseBool, this::scaleToFit);\n    }\n\n    public static EntityComponent<?> parse(Element element) {\n        UIParsing.expectAttributes(element, \"type\");\n        var entityId = UIParsing.parseIdentifier(element.getAttributeNode(\"type\"));\n        var entityType = BuiltInRegistries.ENTITY_TYPE.getOptional(entityId).orElseThrow(() -> new UIModelParsingException(\"Unknown entity type \" + entityId));\n\n        CompoundTag nbt = null;\n        if (element.hasAttribute(\"nbt\")) {\n            try {\n                nbt = TagParser.parseCompoundFully(element.getAttribute(\"nbt\"));\n            } catch (CommandSyntaxException cse) {\n                throw new UIModelParsingException(\"Invalid NBT compound\", cse);\n            }\n        }\n\n        return new EntityComponent<>(Sizing.content(), entityType, nbt);\n    }\n\n    public static class RenderablePlayerEntity extends LocalPlayer {\n\n        protected PlayerSkin skinTextures;\n\n        protected RenderablePlayerEntity(GameProfile profile) {\n            super(Minecraft.getInstance(),\n                Minecraft.getInstance().level,\n                new ClientPacketListener(Minecraft.getInstance(),\n                    new net.minecraft.network.Connection(PacketFlow.CLIENTBOUND),\n                    new CommonListenerCookie(\n                        new LevelLoadTracker(0),\n                        profile, new WorldSessionTelemetryManager(TelemetryEventSender.DISABLED, false, Duration.ZERO, \"\"),\n                        Minecraft.getInstance().level.registryAccess().freeze(),\n                        Minecraft.getInstance().level.enabledFeatures(),\n                        \"Wisp Forest Enterprises\", null, null, Map.of(), null, Map.of(), ServerLinks.EMPTY, Map.of(),\n                        true\n                    )),\n                null, null, Input.EMPTY, false\n            );\n\n            this.skinTextures = DefaultPlayerSkin.get(profile);\n            Util.backgroundExecutor().execute(() -> {\n                var completeProfile = Minecraft.getInstance().services().profileResolver().fetchById(profile.id()).orElse(profile);\n\n                this.skinTextures = DefaultPlayerSkin.get(completeProfile);\n                this.minecraft.getSkinManager().get(completeProfile).thenAccept(textures -> {\n                    textures.ifPresent($ -> this.skinTextures = $);\n                });\n            });\n        }\n\n        @Override\n        public PlayerSkin getSkin() {\n            return this.skinTextures;\n        }\n\n        @Override\n        public boolean isModelPartShown(PlayerModelPart part) {\n            return true;\n        }\n\n        @Nullable\n        @Override\n        protected PlayerInfo getPlayerInfo() {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/ItemComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport com.mojang.brigadier.StringReader;\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.renderstate.OwoItemElementRenderState;\nimport net.fabricmc.fabric.api.client.rendering.v1.TooltipComponentCallback;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.renderer.item.ItemModelResolver;\nimport net.minecraft.client.renderer.item.ItemStackRenderState;\nimport net.minecraft.commands.arguments.item.ItemParser;\nimport net.minecraft.core.HolderLookup;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemDisplayContext;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.TooltipFlag;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.stream.Stream;\n\npublic class ItemComponent extends BaseUIComponent {\n\n    protected final ItemModelResolver itemModelManager;\n    protected ItemStack stack;\n    protected boolean showOverlay = false;\n    protected boolean setTooltipFromStack = false;\n\n    protected ItemComponent(ItemStack stack) {\n        this.itemModelManager = Minecraft.getInstance().getItemModelResolver();\n        this.stack = stack;\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return 16;\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return 16;\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        var matrices = graphics.pose();\n        matrices.pushMatrix();\n\n        // Translate to the root of the component\n        matrices.translate(this.x, this.y);\n\n        // Scale according to component size and translate to the center\n        matrices.scale(this.width / 16f, this.height / 16f);\n\n        var client = Minecraft.getInstance();\n\n        if (this.width <= 16 && this.height <= 16) {\n            graphics.renderItem(this.stack, 0, 0);\n        } else {\n            var state = new ItemStackRenderState();\n            this.itemModelManager.appendItemLayers(state, this.stack, ItemDisplayContext.GUI, Minecraft.getInstance().level, Minecraft.getInstance().player, 0);\n\n            graphics.guiRenderState.submitPicturesInPictureState(new OwoItemElementRenderState(\n                state,\n                new ScreenRectangle(this.x, this.y, this.width, this.height),\n                graphics.scissorStack.peek()\n            ));\n        }\n\n        // Clean up\n        matrices.popMatrix();\n\n        if (this.showOverlay) {\n            graphics.renderItemDecorations(client.font, this.stack, this.x, this.y);\n        }\n    }\n\n    protected void updateTooltipForStack() {\n        if (!this.setTooltipFromStack) return;\n\n        if (!this.stack.isEmpty()) {\n            Minecraft client = Minecraft.getInstance();\n            this.tooltip(tooltipFromItem(this.stack, Item.TooltipContext.of(client.level), client.player, null));\n        } else {\n            this.tooltip((List<ClientTooltipComponent>) null);\n        }\n    }\n\n    public ItemComponent setTooltipFromStack(boolean setTooltipFromStack) {\n        this.setTooltipFromStack = setTooltipFromStack;\n        this.updateTooltipForStack();\n\n        return this;\n    }\n\n    public boolean setTooltipFromStack() {\n        return setTooltipFromStack;\n    }\n\n    public ItemComponent stack(ItemStack stack) {\n        this.stack = stack;\n        this.updateTooltipForStack();\n\n        return this;\n    }\n\n    public ItemStack stack() {\n        return this.stack;\n    }\n\n    public ItemComponent showOverlay(boolean drawOverlay) {\n        this.showOverlay = drawOverlay;\n        return this;\n    }\n\n    public boolean showOverlay() {\n        return this.showOverlay;\n    }\n\n    /**\n     * Obtain the full item stack tooltip, including custom components\n     * provided via {@link net.minecraft.world.item.Item#getTooltipImage(ItemStack)}\n     *\n     * @param stack   The item stack from which to obtain the tooltip\n     * @param context the tooltip context\n     * @param player  The player to use for context, may be {@code null}\n     * @param type    The tooltip type - {@code null} to fall back to the default provided by\n     *                {@link net.minecraft.client.Options#advancedItemTooltips}\n     */\n    public static List<ClientTooltipComponent> tooltipFromItem(ItemStack stack, Item.TooltipContext context, @Nullable Player player, @Nullable TooltipFlag type) {\n        if (type == null) {\n            type = Minecraft.getInstance().options.advancedItemTooltips ? TooltipFlag.ADVANCED : TooltipFlag.NORMAL;\n        }\n\n        var tooltip = new ArrayList<ClientTooltipComponent>();\n        stack.getTooltipLines(context, player, type)\n            .stream()\n            .map(Component::getVisualOrderText)\n            .map(ClientTooltipComponent::create)\n            .forEach(tooltip::add);\n\n        stack.getTooltipImage().ifPresent(data -> {\n            tooltip.add(1, Objects.requireNonNullElseGet(\n                TooltipComponentCallback.EVENT.invoker().getComponent(data),\n                () -> ClientTooltipComponent.create(data)\n            ));\n        });\n\n        return tooltip;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"show-overlay\", UIParsing::parseBool, this::showOverlay);\n        UIParsing.apply(children, \"set-tooltip-from-stack\", UIParsing::parseBool, this::setTooltipFromStack);\n\n        UIParsing.apply(children, \"item\", UIParsing::parseIdentifier, itemId -> {\n            Owo.debugWarn(Owo.LOGGER, \"Deprecated <item> property populated on item component - migrate to <stack> instead\");\n\n            var item = BuiltInRegistries.ITEM.getOptional(itemId).orElseThrow(() -> new UIModelParsingException(\"Unknown item \" + itemId));\n            this.stack(item.getDefaultInstance());\n        });\n\n        UIParsing.apply(children, \"stack\", $ -> $.getTextContent().strip(), stackString -> {\n            try {\n                var result = new ItemParser(HolderLookup.Provider.create(Stream.of(BuiltInRegistries.ITEM)))\n                    .parse(new StringReader(stackString));\n\n                var stack = new ItemStack(result.item());\n                stack.applyComponentsAndValidate(result.components());\n\n                this.stack(stack);\n            } catch (CommandSyntaxException cse) {\n                throw new UIModelParsingException(\"Invalid item stack\", cse);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/LabelComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.braid.widgets.label.RawLabel;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\n\npublic class LabelComponent extends BaseUIComponent {\n\n    protected final Font textRenderer = Minecraft.getInstance().font;\n\n    protected Component text;\n    protected List<FormattedCharSequence> wrappedText;\n\n    protected VerticalAlignment verticalTextAlignment = VerticalAlignment.TOP;\n    protected HorizontalAlignment horizontalTextAlignment = HorizontalAlignment.LEFT;\n\n    protected final AnimatableProperty<Color> color = AnimatableProperty.of(Color.WHITE);\n    protected final Observable<Integer> lineHeight = Observable.of(this.textRenderer.lineHeight);\n    protected final Observable<Integer> lineSpacing = Observable.of(2);\n    protected boolean shadow;\n    protected int maxWidth;\n\n    protected Function<@Nullable Style, Boolean> textClickHandler = style -> {\n        return style != null && OwoUIGraphics.utilityScreen().handleTextClick(style, Minecraft.getInstance().screen);\n    };\n\n    protected LabelComponent(Component text) {\n        this.text = text;\n        this.wrappedText = new ArrayList<>();\n\n        this.shadow = false;\n        this.maxWidth = Integer.MAX_VALUE;\n\n        Observable.observeAll(this::notifyParentIfMounted, this.lineHeight, this.lineSpacing);\n    }\n\n    public LabelComponent text(Component text) {\n        this.text = text;\n        this.notifyParentIfMounted();\n        return this;\n    }\n\n    public Component text() {\n        return this.text;\n    }\n\n    public LabelComponent maxWidth(int maxWidth) {\n        this.maxWidth = maxWidth;\n        this.notifyParentIfMounted();\n        return this;\n    }\n\n    public int maxWidth() {\n        return this.maxWidth;\n    }\n\n    public LabelComponent shadow(boolean shadow) {\n        this.shadow = shadow;\n        return this;\n    }\n\n    public boolean shadow() {\n        return this.shadow;\n    }\n\n    public LabelComponent color(Color color) {\n        this.color.set(color);\n        return this;\n    }\n\n    public AnimatableProperty<Color> color() {\n        return this.color;\n    }\n\n    public LabelComponent verticalTextAlignment(VerticalAlignment verticalAlignment) {\n        this.verticalTextAlignment = verticalAlignment;\n        return this;\n    }\n\n    public VerticalAlignment verticalTextAlignment() {\n        return this.verticalTextAlignment;\n    }\n\n    public LabelComponent horizontalTextAlignment(HorizontalAlignment horizontalAlignment) {\n        this.horizontalTextAlignment = horizontalAlignment;\n        return this;\n    }\n\n    public HorizontalAlignment horizontalTextAlignment() {\n        return this.horizontalTextAlignment;\n    }\n\n    public LabelComponent lineHeight(int lineHeight) {\n        this.lineHeight.set(lineHeight);\n        return this;\n    }\n\n    public int lineHeight() {\n        return this.lineHeight.get();\n    }\n\n    public LabelComponent lineSpacing(int lineSpacing) {\n        this.lineSpacing.set(lineSpacing);\n        return this;\n    }\n\n    public int lineSpacing() {\n        return this.lineSpacing.get();\n    }\n\n    public LabelComponent textClickHandler(Function<@Nullable Style, Boolean> textClickHandler) {\n        this.textClickHandler = textClickHandler;\n        return this;\n    }\n\n    public Function<Style, Boolean> textClickHandler() {\n        return textClickHandler;\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        int widestText = 0;\n        for (var line : this.wrappedText) {\n            int width = this.textRenderer.width(line);\n            if (width > widestText) widestText = width;\n        }\n\n        if (widestText > this.maxWidth) {\n            this.wrapLines();\n            return this.determineHorizontalContentSize(sizing);\n        } else {\n            return widestText;\n        }\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        this.wrapLines();\n        return this.textHeight();\n    }\n\n    @Override\n    public void inflate(Size space) {\n        this.wrapLines();\n        super.inflate(space);\n    }\n\n    private void wrapLines() {\n        this.wrappedText = this.textRenderer.split(this.text, this.horizontalSizing.get().isContent() ? this.maxWidth : this.width);\n    }\n\n    protected int textHeight() {\n        return (this.wrappedText.size() * (this.lineHeight() + this.lineSpacing())) - this.lineSpacing();\n    }\n\n    @Override\n    public void update(float delta, int mouseX, int mouseY) {\n        super.update(delta, mouseX, mouseY);\n        this.color.update(delta);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        graphics\n            .push()\n            .translate(0, 1f / Minecraft.getInstance().getWindow().getGuiScale());\n\n        this.drawText((renderX, renderY, text, shadow, color) -> {\n            graphics.drawString(\n                Minecraft.getInstance().font,\n                text,\n                renderX,\n                renderY,\n                color.argb(),\n                shadow\n            );\n        });\n\n        graphics.pop();\n    }\n\n    protected void drawText(LabelDrawFunction goodFunction) {\n        int x = this.x;\n        int y = this.y;\n\n        if (this.horizontalSizing.get().isContent()) {\n            x += this.horizontalSizing.get().value;\n        }\n        if (this.verticalSizing.get().isContent()) {\n            y += this.verticalSizing.get().value;\n        }\n\n        switch (this.verticalTextAlignment) {\n            case CENTER -> y += (this.height - (this.textHeight())) / 2;\n            case BOTTOM -> y += this.height - (this.textHeight());\n        }\n\n        final int lambdaX = x;\n        final int lambdaY = y;\n\n        for (int i = 0; i < this.wrappedText.size(); i++) {\n            var renderText = this.wrappedText.get(i);\n            int renderX = lambdaX;\n\n            switch (this.horizontalTextAlignment) {\n                case CENTER -> renderX += (this.width - this.textRenderer.width(renderText)) / 2;\n                case RIGHT -> renderX += this.width - this.textRenderer.width(renderText);\n            }\n\n            int renderY = lambdaY + i * (this.lineHeight() + this.lineSpacing());\n            renderY += this.lineHeight() - this.textRenderer.lineHeight;\n\n            goodFunction.draw(renderX, renderY, renderText, this.shadow, this.color.get());\n        }\n    }\n\n    @Override\n    public void drawTooltip(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.drawTooltip(context, mouseX, mouseY, partialTicks, delta);\n        context.renderComponentHoverEffect(this.textRenderer, this.styleAt(mouseX - this.x, mouseY - this.y), mouseX, mouseY);\n    }\n\n    @Override\n    public boolean shouldDrawTooltip(double mouseX, double mouseY) {\n        var hoveredStyle = this.styleAt((int) (mouseX - this.x), (int) (mouseY - this.y));\n        return super.shouldDrawTooltip(mouseX, mouseY) || (hoveredStyle != null && hoveredStyle.getHoverEvent() != null && this.isInBoundingBox(mouseX, mouseY));\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        return this.textClickHandler.apply(this.styleAt((int) click.x(), (int) click.y())) | super.onMouseDown(click, doubled);\n    }\n\n    @Nullable\n    protected Style styleAt(int mouseX, int mouseY) {\n        var clickHandler = new RawLabel.Instance.StyleCollector(this.textRenderer, this.x + mouseX, this.y + mouseY);\n        this.drawText((renderX, renderY, text, $, $$) -> clickHandler.accept(renderX, renderY, text));\n\n        return clickHandler.result();\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"text\", UIParsing::parseText, this::text);\n        UIParsing.apply(children, \"max-width\", UIParsing::parseUnsignedInt, this::maxWidth);\n        UIParsing.apply(children, \"color\", Color::parse, this::color);\n        UIParsing.apply(children, \"shadow\", UIParsing::parseBool, this::shadow);\n        UIParsing.apply(children, \"line-height\", UIParsing::parseUnsignedInt, this::lineHeight);\n        UIParsing.apply(children, \"line-spacing\", UIParsing::parseUnsignedInt, this::lineSpacing);\n\n        UIParsing.apply(children, \"vertical-text-alignment\", VerticalAlignment::parse, this::verticalTextAlignment);\n        UIParsing.apply(children, \"horizontal-text-alignment\", HorizontalAlignment::parse, this::horizontalTextAlignment);\n    }\n\n    @FunctionalInterface\n    protected interface LabelDrawFunction {\n        void draw(int renderX, int renderY, FormattedCharSequence text, boolean shadow, Color color);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/SliderComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.gui.components.AbstractSliderButton;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.input.MouseButtonInfo;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.util.Mth;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\nimport java.util.function.Function;\n\npublic class SliderComponent extends AbstractSliderButton {\n\n    protected final EventStream<OnChanged> changedEvents = OnChanged.newStream();\n    protected final EventStream<OnSlideEnd> slideEndEvents = OnSlideEnd.newStream();\n\n    protected Function<String, Component> messageProvider = value -> Component.empty();\n    protected double scrollStep = .05;\n\n    protected SliderComponent(Sizing horizontalSizing) {\n        super(0, 0, 0, 0, Component.empty(), 0);\n\n        this.sizing(horizontalSizing, Sizing.fixed(20));\n    }\n\n    public SliderComponent value(double value) {\n        value = Mth.clamp(value, 0, 1);\n\n        if (this.value != value) {\n            this.value = value;\n\n            this.updateMessage();\n            this.applyValue();\n        }\n\n        return this;\n    }\n\n    public double value() {\n        return this.value;\n    }\n\n    public SliderComponent message(Function<String, Component> messageProvider) {\n        this.messageProvider = messageProvider;\n        this.updateMessage();\n        return this;\n    }\n\n    public SliderComponent scrollStep(double scrollStep) {\n        this.scrollStep = scrollStep;\n        return this;\n    }\n\n    public double scrollStep() {\n        return this.scrollStep;\n    }\n\n    public SliderComponent active(boolean active) {\n        this.active = active;\n        return this;\n    }\n\n    public boolean active() {\n        return this.active;\n    }\n\n    public EventSource<OnChanged> onChanged() {\n        return this.changedEvents.source();\n    }\n\n    public EventSource<OnSlideEnd> slideEnd() {\n        return this.slideEndEvents.source();\n    }\n\n    @Override\n    protected void updateMessage() {\n        this.setMessage(this.messageProvider.apply(String.valueOf(this.value)));\n    }\n\n    @Override\n    protected void applyValue() {\n        this.changedEvents.sink().onChanged(this.value);\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        if (!this.active) return super.onMouseScroll(mouseX, mouseY, amount);\n\n        this.value(this.value + this.scrollStep * amount);\n\n        super.onMouseScroll(mouseX, mouseY, amount);\n        return true;\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        this.slideEndEvents.sink().onSlideEnd();\n        return super.onMouseUp(click);\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (!this.active) return false;\n        return super.keyPressed(input);\n    }\n\n    @Override\n    protected boolean isValidClickButton(MouseButtonInfo input) {\n        return this.active && super.isValidClickButton(input);\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        if (children.containsKey(\"text\")) {\n            var node = children.get(\"text\");\n            var content = node.getTextContent().strip();\n\n            if (node.getAttribute(\"translate\").equalsIgnoreCase(\"true\")) {\n                this.message(value -> Component.translatable(content, value));\n            } else {\n                var text = Component.literal(content);\n                this.message(value -> text);\n            }\n        }\n\n        UIParsing.apply(children, \"value\", UIParsing::parseDouble, this::value);\n    }\n\n    /**\n     * @deprecated Use {@link #message(Function)} instead,\n     * as the message set by this method will be overwritten\n     * the next time this slider is moved\n     */\n    @Override\n    @Deprecated\n    public final void setMessage(Component message) {\n        super.setMessage(message);\n    }\n\n    public interface OnChanged {\n        void onChanged(double value);\n\n        static EventStream<OnChanged> newStream() {\n            return new EventStream<>(subscribers -> value -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onChanged(value);\n                }\n            });\n        }\n    }\n\n    public interface OnSlideEnd {\n        void onSlideEnd();\n\n        static EventStream<OnSlideEnd> newStream() {\n            return new EventStream<>(subscribers -> () -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onSlideEnd();\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/SlimSliderComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\n\npublic class SlimSliderComponent extends BaseUIComponent {\n\n    public static final Function<Double, Component> VALUE_TOOLTIP_SUPPLIER = value -> Component.literal(String.valueOf(value));\n\n    protected static final Identifier TEXTURE = Owo.id(\"textures/gui/slim_slider.png\");\n    protected static final Identifier TRACK_TEXTURE = Owo.id(\"slim_slider_track\");\n\n    protected final EventStream<OnChanged> changedEvents = OnChanged.newStream();\n    protected final EventStream<OnSlideEnd> slideEndEvents = OnSlideEnd.newStream();\n\n    protected final Axis axis;\n    protected final Observable<Double> value = Observable.of(0d);\n\n    protected double min = 0d, max = 1d;\n    protected double stepSize = 0;\n    protected @Nullable Function<Double, Component> tooltipSupplier = null;\n\n    public SlimSliderComponent(Axis axis) {\n        this.cursorStyle(CursorStyle.MOVE);\n\n        this.axis = axis;\n        this.value.observe($ -> {\n            this.changedEvents.sink().onChanged(this.value());\n            this.updateTooltip();\n        });\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        if (this.axis == Axis.VERTICAL) {\n            return 9;\n        } else {\n            throw new UnsupportedOperationException(\"Horizontal SlimSliderComponent cannot be horizontally content-sized\");\n        }\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        if (this.axis == Axis.HORIZONTAL) {\n            return 9;\n        } else {\n            throw new UnsupportedOperationException(\"Vertical SlimSliderComponent cannot be vertically content-sized\");\n        }\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        if (this.axis == Axis.HORIZONTAL) {\n            NinePatchTexture.draw(TRACK_TEXTURE, graphics, this.x + 1, this.y + 3, this.width - 2, 3);\n            graphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, (int) (this.x + (this.width - 4) * this.value.get()), this.y + 1, 0, 3, 4, 7, 4, 7, 16, 16);\n        } else {\n            NinePatchTexture.draw(TRACK_TEXTURE, graphics, this.x + 3, this.y + 1, 3, this.height - 2);\n            graphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, this.x + 1, (int) (this.y + (this.height - 4) * this.value.get()), 4, 3, 7, 4, 7, 4, 16, 16);\n        }\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        super.onMouseDown(click, doubled);\n        this.setValueFromMouse(click.x(), click.y());\n        return true;\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        super.onMouseDrag(click, deltaX, deltaY);\n        this.setValueFromMouse(click.x(), click.y());\n        return true;\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        super.onMouseUp(click);\n        this.slideEndEvents.sink().onSlideEnd();\n        return true;\n    }\n\n    protected void setValueFromMouse(double mouseX, double mouseY) {\n        this.value(this.axis == Axis.VERTICAL\n            ? this.min + (mouseY / this.height) * (this.max - this.min)\n            : this.min + (mouseX / this.width) * (this.max - this.min));\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return true;\n    }\n\n    public EventSource<OnChanged> onChanged() {\n        return this.changedEvents.source();\n    }\n\n    public EventSource<OnSlideEnd> onSlideEnd() {\n        return this.slideEndEvents.source();\n    }\n\n    public SlimSliderComponent value(double value) {\n        value -= this.min;\n        if (this.stepSize != 0) {\n            value = Math.round(value / this.stepSize) * this.stepSize;\n        }\n\n        this.value.set(Mth.clamp(value / (this.max - this.min), 0, 1));\n        return this;\n    }\n\n    public double value() {\n        return this.min + this.value.get() * (this.max - this.min);\n    }\n\n    public SlimSliderComponent min(double min) {\n        this.min = min;\n        return this;\n    }\n\n    public double min() {\n        return min;\n    }\n\n    public SlimSliderComponent max(double max) {\n        this.max = max;\n        return this;\n    }\n\n    public double max() {\n        return max;\n    }\n\n    public SlimSliderComponent stepSize(double stepSize) {\n        this.stepSize = stepSize;\n        return this;\n    }\n\n    public double stepSize() {\n        return stepSize;\n    }\n\n    public SlimSliderComponent tooltipSupplier(Function<Double, Component> tooltipSupplier) {\n        this.tooltipSupplier = tooltipSupplier;\n        this.updateTooltip();\n\n        return this;\n    }\n\n    public Function<Double, Component> tooltipSupplier() {\n        return tooltipSupplier;\n    }\n\n    protected void updateTooltip() {\n        if (this.tooltipSupplier != null) {\n            this.tooltip(this.tooltipSupplier.apply(this.value()));\n        } else {\n            this.tooltip((List<ClientTooltipComponent>) null);\n        }\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"step-size\", UIParsing::parseDouble, this::stepSize);\n        UIParsing.apply(children, \"min\", UIParsing::parseDouble, this::min);\n        UIParsing.apply(children, \"max\", UIParsing::parseDouble, this::max);\n        UIParsing.apply(children, \"value\", UIParsing::parseDouble, this::value);\n    }\n\n    public static UIComponent parse(Element element) {\n        return element.getAttribute(\"direction\").equals(\"vertical\")\n            ? new SlimSliderComponent(Axis.VERTICAL)\n            : new SlimSliderComponent(Axis.HORIZONTAL);\n    }\n\n    public static Function<Double, Component> valueTooltipSupplier(int decimalPlaces) {\n        return value -> Component.literal(new BigDecimal(value).setScale(decimalPlaces, RoundingMode.HALF_UP).toPlainString());\n    }\n\n    public enum Axis {\n        VERTICAL, HORIZONTAL\n    }\n\n    public interface OnChanged {\n        void onChanged(double value);\n\n        static EventStream<OnChanged> newStream() {\n            return new EventStream<>(subscribers -> value -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onChanged(value);\n                }\n            });\n        }\n    }\n\n    public interface OnSlideEnd {\n        void onSlideEnd();\n\n        static EventStream<OnSlideEnd> newStream() {\n            return new EventStream<>(subscribers -> () -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onSlideEnd();\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/SmallCheckboxComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.UISounds;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\n\npublic class SmallCheckboxComponent extends BaseUIComponent {\n\n    public static final Identifier TEXTURE = Owo.id(\"textures/gui/smol_checkbox.png\");\n\n    protected final EventStream<OnChanged> checkedEvents = OnChanged.newStream();\n\n    protected final Observable<@Nullable Component> label;\n    protected boolean labelShadow = false;\n    protected boolean checked = false;\n\n    public SmallCheckboxComponent(Component label) {\n        this.cursorStyle(CursorStyle.HAND);\n\n        this.label = Observable.of(label);\n        this.label.observe(text -> this.notifyParentIfMounted());\n    }\n\n    public SmallCheckboxComponent() {\n        this(null);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        if (this.label.get() != null) {\n            graphics.drawString(Minecraft.getInstance().font, this.label.get(), this.x + 13 + 2, this.y + 3, Color.WHITE.argb(), this.labelShadow);\n        }\n\n        graphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, this.x, this.y, 0, 0, 13, 13, 13, 13, 32, 16);\n        if (this.checked) {\n            graphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, this.x, this.y, 16, 0, 13, 13, 13, 13, 32, 16);\n        }\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.label.get() != null\n                ? 13 + 2 + Minecraft.getInstance().font.width(this.label.get())\n                : 13;\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return 13;\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        boolean result = super.onMouseDown(click, doubled);\n\n        if (click.button() == GLFW.GLFW_MOUSE_BUTTON_LEFT) {\n            this.toggle();\n            return true;\n        }\n\n        return result;\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        boolean result = super.onKeyPress(input);\n\n        if (input.isSelection()) {\n            this.toggle();\n            return true;\n        }\n\n        return result;\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return true;\n    }\n\n    public void toggle() {\n        this.checked(!this.checked);\n        UISounds.playInteractionSound();\n    }\n\n    public EventSource<OnChanged> onChanged() {\n        return this.checkedEvents.source();\n    }\n\n    public SmallCheckboxComponent checked(boolean checked) {\n        this.checked = checked;\n        this.checkedEvents.sink().onChanged(this.checked);\n\n        return this;\n    }\n\n    public boolean checked() {\n        return checked;\n    }\n\n    public SmallCheckboxComponent label(Component label) {\n        this.label.set(label);\n        return this;\n    }\n\n    public Component label() {\n        return this.label.get();\n    }\n\n    public SmallCheckboxComponent labelShadow(boolean labelShadow) {\n        this.labelShadow = labelShadow;\n        return this;\n    }\n\n    public boolean labelShadow() {\n        return labelShadow;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"label\", UIParsing::parseText, this::label);\n        UIParsing.apply(children, \"label-shadow\", UIParsing::parseBool, this::labelShadow);\n        UIParsing.apply(children, \"checked\", UIParsing::parseBool, this::checked);\n    }\n\n    public interface OnChanged {\n        void onChanged(boolean nowChecked);\n\n        static EventStream<OnChanged> newStream() {\n            return new EventStream<>(subscribers -> value -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onChanged(value);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/SpacerComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport org.w3c.dom.Element;\n\npublic class SpacerComponent extends BaseUIComponent {\n\n    protected SpacerComponent(int percent) {\n        this.sizing(Sizing.expand(percent));\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {}\n\n    public static SpacerComponent parse(Element element) {\n        if (!element.hasAttribute(\"percent\")) return UIComponents.spacer();\n        return UIComponents.spacer(UIParsing.parseUnsignedInt(element.getAttributeNode(\"percent\")));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/SpriteComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.OwoUIPipelines;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.SpriteUtilInvoker;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.client.renderer.texture.TextureAtlasSprite;\nimport net.minecraft.client.resources.model.Material;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\n\npublic class SpriteComponent extends BaseUIComponent {\n\n    protected final TextureAtlasSprite sprite;\n    protected boolean blend = false;\n\n    protected SpriteComponent(TextureAtlasSprite sprite) {\n        this.sprite = sprite;\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.sprite.contents().width();\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return this.sprite.contents().height();\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        SpriteUtilInvoker.markSpriteActive(this.sprite);\n        graphics.blitSprite(this.blend ? RenderPipelines.GUI_TEXTURED : OwoUIPipelines.GUI_TEXTURED_NO_BLEND, this.sprite, this.x, this.y, this.width, this.height);\n    }\n\n    public SpriteComponent blend(boolean blend) {\n        this.blend = blend;\n        return this;\n    }\n\n    public boolean blend() {\n        return this.blend;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"blend\", UIParsing::parseBool, this::blend);\n    }\n\n    public static SpriteComponent parse(Element element) {\n        UIParsing.expectAttributes(element, \"atlas\", \"sprite\");\n\n        var atlas = UIParsing.parseIdentifier(element.getAttributeNode(\"atlas\"));\n        var sprite = UIParsing.parseIdentifier(element.getAttributeNode(\"sprite\"));\n\n        return UIComponents.sprite(new Material(atlas, sprite));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/TextAreaComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.mixin.ui.access.MultiLineEditBoxAccessor;\nimport io.wispforest.owo.mixin.ui.access.MultilineTextFieldAccessor;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.Size;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.MultiLineEditBox;\nimport net.minecraft.client.gui.components.MultilineTextField;\nimport net.minecraft.client.gui.components.Whence;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic class TextAreaComponent extends MultiLineEditBox {\n\n    protected final Observable<String> textValue = Observable.of(\"\");\n    protected final EventStream<OnChanged> changedEvents = OnChanged.newStream();\n    protected final MultilineTextField editBox;\n\n    protected final Observable<Boolean> displayCharCount = Observable.of(false);\n    protected final Observable<Integer> maxLines = Observable.of(-1);\n\n    protected TextAreaComponent(Sizing horizontalSizing, Sizing verticalSizing) {\n        super(Minecraft.getInstance().font, 0, 0, 0, 0, Component.empty(), Component.empty(), Color.WHITE.argb(), false, Color.WHITE.argb(), true, true);\n        this.editBox = ((MultiLineEditBoxAccessor) this).owo$getTextField();\n        this.sizing(horizontalSizing, verticalSizing);\n\n        this.textValue.observe(this.changedEvents.sink()::onChanged);\n        Observable.observeAll(this.widgetWrapper()::notifyParentIfMounted, this.displayCharCount, this.maxLines);\n\n        super.setValueListener(s -> {\n            this.textValue.set(s);\n\n            if (this.maxLines.get() < 0) return;\n            this.widgetWrapper().notifyParentIfMounted();\n        });\n    }\n\n    @Override\n    @Deprecated(forRemoval = true)\n    public void setValueListener(Consumer<String> changeListener) {\n        Owo.debugWarn(Owo.LOGGER, \"setChangeListener stub on TextAreaComponent invoked\");\n    }\n\n    @Override\n    public void update(float delta, int mouseX, int mouseY) {\n        super.update(delta, mouseX, mouseY);\n        this.cursorStyle(this.scrollbarVisible() && mouseX >= this.getX() + this.width - 9 ? CursorStyle.NONE : CursorStyle.TEXT);\n    }\n\n    @Override\n    protected void renderDecorations(GuiGraphics context) {\n        this.height -= 1;\n\n        var matrices = context.pose();\n        matrices.pushMatrix();\n        matrices.translate(-9, 1);\n\n        int previousMaxLength = this.editBox.characterLimit();\n        this.editBox.setCharacterLimit(Integer.MAX_VALUE);\n\n        super.renderDecorations(context);\n\n        this.editBox.setCharacterLimit(previousMaxLength);\n\n        matrices.popMatrix();\n        this.height += 1;\n\n        if (this.displayCharCount.get()) {\n            var text = this.editBox.hasCharacterLimit()\n                    ? Component.translatable(\"gui.multiLineEditBox.character_limit\", this.editBox.value().length(), this.editBox.characterLimit())\n                    : Component.literal(String.valueOf(this.editBox.value().length()));\n\n            var textRenderer = Minecraft.getInstance().font;\n            context.drawString(textRenderer, text, this.getX() + this.width - textRenderer.width(text), this.getY() + this.height + 3, 0xa0a0a0);\n        }\n    }\n\n    @Override\n    public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {\n        this.width -= 9;\n        var result = super.mouseClicked(click, doubled);\n        this.width += 9;\n\n        return result;\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        boolean result = super.keyPressed(input);\n\n        if (input.isCycleFocus()) {\n            this.editBox.insertText(\"    \");\n            return true;\n        } else {\n            return result;\n        }\n    }\n\n    @Override\n    public void inflate(Size space) {\n        super.inflate(space);\n\n        int cursor = this.editBox.cursor();\n        int selection = ((MultilineTextFieldAccessor) this.editBox).owo$getSelectCursor();\n\n        ((MultilineTextFieldAccessor) this.editBox).owo$setWidth(this.width() - this.totalInnerPadding() - 9);\n        this.editBox.setValue(this.getValue(), false);\n\n        super.inflate(space);\n        this.editBox.setValue(this.getValue(), false);\n\n        this.editBox.seekCursor(Whence.ABSOLUTE, cursor);\n        ((MultilineTextFieldAccessor) this.editBox).owo$setSelectCursor(selection);\n    }\n\n    public EventSource<OnChanged> onChanged() {\n        return changedEvents.source();\n    }\n\n    public TextAreaComponent maxLines(int maxLines) {\n        this.maxLines.set(maxLines);\n        return this;\n    }\n\n    public int maxLines() {\n        return this.maxLines.get();\n    }\n\n    public TextAreaComponent displayCharCount(boolean displayCharCount) {\n        this.displayCharCount.set(displayCharCount);\n        return this;\n    }\n\n    public boolean displayCharCount() {\n        return this.displayCharCount.get();\n    }\n\n    public TextAreaComponent text(String text) {\n        this.setValue(text);\n        return this;\n    }\n\n    @Override\n    public int heightOffset() {\n        return this.displayCharCount.get() ? -12 : 0;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"display-char-count\", UIParsing::parseBool, this::displayCharCount);\n        UIParsing.apply(children, \"max-length\", UIParsing::parseUnsignedInt, this::setCharacterLimit);\n        UIParsing.apply(children, \"max-lines\", UIParsing::parseUnsignedInt, this::maxLines);\n        UIParsing.apply(children, \"text\", $ -> $.getTextContent().strip(), this::text);\n    }\n\n    public interface OnChanged {\n        void onChanged(String value);\n\n        static EventStream<OnChanged> newStream() {\n            return new EventStream<>(subscribers -> value -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onChanged(value);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/TextBoxComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.mixin.ui.access.EditBoxAccessor;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.network.chat.Component;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic class TextBoxComponent extends EditBox {\n\n    protected final Observable<Boolean> showsBackground = Observable.of(((EditBoxAccessor) this).owo$bordered());\n\n    protected final Observable<String> textValue = Observable.of(\"\");\n    protected final EventStream<OnChanged> changedEvents = OnChanged.newStream();\n\n    protected TextBoxComponent(Sizing horizontalSizing) {\n        super(Minecraft.getInstance().font, 0, 0, 0, 0, Component.empty());\n\n        this.textValue.observe(this.changedEvents.sink()::onChanged);\n        this.sizing(horizontalSizing, Sizing.content());\n\n        this.showsBackground.observe(a -> this.widgetWrapper().notifyParentIfMounted());\n    }\n\n    /**\n     * @deprecated Subscribe to {@link #onChanged()} instead\n     */\n    @Override\n    @Deprecated(forRemoval = true)\n    public void setResponder(Consumer<String> changedListener) {\n        super.setResponder(changedListener);\n    }\n\n    @Override\n    public void drawFocusHighlight(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {\n        // noop, since TextFieldWidget already does this\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        boolean result = super.keyPressed(input);\n\n        if (input.isCycleFocus()) {\n            this.insertText(\"    \");\n            return true;\n        } else {\n            return result;\n        }\n    }\n\n    @Override\n    public void updateX(int x) {\n        super.updateX(x);\n        ((EditBoxAccessor) this).owo$updateTextPosition();\n    }\n\n    @Override\n    public void updateY(int y) {\n        super.updateY(y);\n        ((EditBoxAccessor) this).owo$updateTextPosition();\n    }\n\n    @Override\n    public void setBordered(boolean drawsBackground) {\n        super.setBordered(drawsBackground);\n        this.showsBackground.set(drawsBackground);\n    }\n\n    public EventSource<OnChanged> onChanged() {\n        return changedEvents.source();\n    }\n\n    public TextBoxComponent text(String text) {\n        this.setValue(text);\n        this.moveCursorToStart(false);\n        return this;\n    }\n\n    @Override\n    public void parseProperties(UIModel spec, Element element, Map<String, Element> children) {\n        super.parseProperties(spec, element, children);\n        UIParsing.apply(children, \"show-background\", UIParsing::parseBool, this::setBordered);\n        UIParsing.apply(children, \"max-length\", UIParsing::parseUnsignedInt, this::setMaxLength);\n        UIParsing.apply(children, \"text\", e -> e.getTextContent().strip(), this::text);\n    }\n\n    protected CursorStyle owo$preferredCursorStyle() {\n        return CursorStyle.TEXT;\n    }\n\n    public interface OnChanged {\n        void onChanged(String value);\n\n        static EventStream<OnChanged> newStream() {\n            return new EventStream<>(subscribers -> value -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onChanged(value);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/TextureComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.resources.Identifier;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\n\npublic class TextureComponent extends BaseUIComponent {\n\n    protected final Identifier texture;\n    protected final int u, v;\n    protected final int regionWidth, regionHeight;\n    protected final int textureWidth, textureHeight;\n\n    protected final AnimatableProperty<PositionedRectangle> visibleArea;\n    protected boolean blend = false;\n\n    protected TextureComponent(Identifier texture, int u, int v, int regionWidth, int regionHeight, int textureWidth, int textureHeight) {\n        this.texture = texture;\n        this.u = u;\n        this.v = v;\n        this.regionWidth = regionWidth;\n        this.regionHeight = regionHeight;\n        this.textureWidth = textureWidth;\n        this.textureHeight = textureHeight;\n\n        this.visibleArea = AnimatableProperty.of(PositionedRectangle.of(0, 0, this.regionWidth, this.regionHeight));\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.regionWidth;\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return this.regionHeight;\n    }\n\n    @Override\n    public void update(float delta, int mouseX, int mouseY) {\n        super.update(delta, mouseX, mouseY);\n        this.visibleArea.update(delta);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        var matrices = graphics.pose();\n        matrices.pushMatrix();\n        matrices.translate(x, y);\n        matrices.scale(this.width / (float) this.regionWidth, this.height / (float) this.regionHeight);\n\n        var visibleArea = this.visibleArea.get();\n\n        int bottomEdge = Math.min(visibleArea.y() + visibleArea.height(), regionHeight);\n        int rightEdge = Math.min(visibleArea.x() + visibleArea.width(), regionWidth);\n\n        graphics.blit(this.blend ? RenderPipelines.GUI_TEXTURED : OwoUIPipelines.GUI_TEXTURED_NO_BLEND,\n            this.texture,\n            visibleArea.x(),\n            visibleArea.y(),\n            this.u + visibleArea.x(),\n            this.v + visibleArea.y(),\n            rightEdge - visibleArea.x(),\n            bottomEdge - visibleArea.y(),\n            rightEdge - visibleArea.x(),\n            bottomEdge - visibleArea.y(),\n            this.textureWidth, this.textureHeight\n        );\n\n        matrices.popMatrix();\n    }\n\n    public TextureComponent visibleArea(PositionedRectangle visibleArea) {\n        this.visibleArea.set(visibleArea);\n        return this;\n    }\n\n    public TextureComponent resetVisibleArea() {\n        this.visibleArea(PositionedRectangle.of(0, 0, this.regionWidth, this.regionHeight));\n        return this;\n    }\n\n    public AnimatableProperty<PositionedRectangle> visibleArea() {\n        return this.visibleArea;\n    }\n\n    public TextureComponent blend(boolean blend) {\n        this.blend = blend;\n        return this;\n    }\n\n    public boolean blend() {\n        return this.blend;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"blend\", UIParsing::parseBool, this::blend);\n\n        if (children.containsKey(\"visible-area\")) {\n            var areaChildren = UIParsing.childElements(children.get(\"visible-area\"));\n\n            int x = 0, y = 0, width = this.regionWidth, height = this.regionHeight;\n            if (areaChildren.containsKey(\"x\")) {\n                x = UIParsing.parseSignedInt(areaChildren.get(\"x\"));\n            }\n\n            if (areaChildren.containsKey(\"y\")) {\n                y = UIParsing.parseSignedInt(areaChildren.get(\"y\"));\n            }\n\n            if (areaChildren.containsKey(\"width\")) {\n                width = UIParsing.parseSignedInt(areaChildren.get(\"width\"));\n            }\n\n            if (areaChildren.containsKey(\"height\")) {\n                height = UIParsing.parseSignedInt(areaChildren.get(\"height\"));\n            }\n\n            this.visibleArea(PositionedRectangle.of(x, y, width, height));\n        }\n    }\n\n    public static TextureComponent parse(Element element) {\n        UIParsing.expectAttributes(element, \"texture\");\n        var textureId = UIParsing.parseIdentifier(element.getAttributeNode(\"texture\"));\n\n        int u = 0, v = 0, regionWidth = 0, regionHeight = 0, textureWidth = 256, textureHeight = 256;\n        if (element.hasAttribute(\"u\")) {\n            u = UIParsing.parseSignedInt(element.getAttributeNode(\"u\"));\n        }\n\n        if (element.hasAttribute(\"v\")) {\n            v = UIParsing.parseSignedInt(element.getAttributeNode(\"v\"));\n        }\n\n        if (element.hasAttribute(\"region-width\")) {\n            regionWidth = UIParsing.parseSignedInt(element.getAttributeNode(\"region-width\"));\n        }\n\n        if (element.hasAttribute(\"region-height\")) {\n            regionHeight = UIParsing.parseSignedInt(element.getAttributeNode(\"region-height\"));\n        }\n\n        if (element.hasAttribute(\"texture-width\")) {\n            textureWidth = UIParsing.parseSignedInt(element.getAttributeNode(\"texture-width\"));\n        }\n\n        if (element.hasAttribute(\"texture-height\")) {\n            textureHeight = UIParsing.parseSignedInt(element.getAttributeNode(\"texture-height\"));\n        }\n\n        return new TextureComponent(textureId, u, v, regionWidth, regionHeight, textureWidth, textureHeight);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/UIComponents.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.renderer.texture.TextureAtlasSprite;\nimport net.minecraft.client.resources.model.Material;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.level.block.EntityBlock;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.state.BlockState;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n// TODO paginated and tabbed containers\n\n/**\n * Utility methods for creating UI components\n */\npublic final class UIComponents {\n\n    private UIComponents() {}\n\n    // -----------------------\n    // Wrapped Vanilla Widgets\n    // -----------------------\n\n    public static ButtonComponent button(Component message, Consumer<ButtonComponent> onPress) {\n        return new ButtonComponent(message, onPress);\n    }\n\n    public static TextBoxComponent textBox(Sizing horizontalSizing) {\n        return new TextBoxComponent(horizontalSizing);\n    }\n\n    public static TextBoxComponent textBox(Sizing horizontalSizing, String text) {\n        var textBox = new TextBoxComponent(horizontalSizing);\n        textBox.text(text);\n        return textBox;\n    }\n\n    public static TextAreaComponent textArea(Sizing horizontalSizing, Sizing verticalSizing) {\n        return new TextAreaComponent(horizontalSizing, verticalSizing);\n    }\n\n    public static TextAreaComponent textArea(Sizing horizontalSizing, Sizing verticalSizing, String text) {\n        var textArea = new TextAreaComponent(horizontalSizing, verticalSizing);\n        textArea.setValue(text);\n        return textArea;\n    }\n\n    // ------------------\n    // Default Components\n    // ------------------\n\n    public static <E extends Entity> EntityComponent<E> entity(Sizing sizing, EntityType<E> type, @Nullable CompoundTag nbt) {\n        return new EntityComponent<>(sizing, type, nbt);\n    }\n\n    public static <E extends Entity> EntityComponent<E> entity(Sizing sizing, E entity) {\n        return new EntityComponent<>(sizing, entity);\n    }\n\n    public static ItemComponent item(ItemStack item) {\n        return new ItemComponent(item);\n    }\n\n    public static BlockComponent block(BlockState state) {\n        return new BlockComponent(state, null);\n    }\n\n    public static BlockComponent block(BlockState state, BlockEntity blockEntity) {\n        return new BlockComponent(state, blockEntity);\n    }\n\n    public static BlockComponent block(BlockState state, @Nullable CompoundTag nbt) {\n        final var client = Minecraft.getInstance();\n\n        BlockEntity blockEntity = null;\n\n        if (state.getBlock() instanceof EntityBlock provider) {\n            blockEntity = provider.newBlockEntity(client.player.blockPosition(), state);\n            BlockComponent.prepareBlockEntity(state, blockEntity, nbt);\n        }\n\n        return new BlockComponent(state, blockEntity);\n    }\n\n    public static LabelComponent label(net.minecraft.network.chat.Component text) {\n        return new LabelComponent(text);\n    }\n\n    public static CheckboxComponent checkbox(net.minecraft.network.chat.Component message) {\n        return new CheckboxComponent(message);\n    }\n\n    public static SliderComponent slider(Sizing horizontalSizing) {\n        return new SliderComponent(horizontalSizing);\n    }\n\n    public static DiscreteSliderComponent discreteSlider(Sizing horizontalSizing, double min, double max) {\n        return new DiscreteSliderComponent(horizontalSizing, min, max);\n    }\n\n    public static SpriteComponent sprite(Material spriteId) {\n        return new SpriteComponent(Minecraft.getInstance().getAtlasManager().get(spriteId));\n    }\n\n    public static SpriteComponent sprite(TextureAtlasSprite sprite) {\n        return new SpriteComponent(sprite);\n    }\n\n    public static TextureComponent texture(Identifier texture, int u, int v, int regionWidth, int regionHeight, int textureWidth, int textureHeight) {\n        return new TextureComponent(texture, u, v, regionWidth, regionHeight, textureWidth, textureHeight);\n    }\n\n    public static TextureComponent texture(Identifier texture, int u, int v, int regionWidth, int regionHeight) {\n        return new TextureComponent(texture, u, v, regionWidth, regionHeight, 256, 256);\n    }\n\n    public static BoxComponent box(Sizing horizontalSizing, Sizing verticalSizing) {\n        return new BoxComponent(horizontalSizing, verticalSizing);\n    }\n\n    public static DropdownComponent dropdown(Sizing horizontalSizing) {\n        return new DropdownComponent(horizontalSizing);\n    }\n\n    public static SlimSliderComponent slimSlider(SlimSliderComponent.Axis axis) {\n        return new SlimSliderComponent(axis);\n    }\n\n    public static SmallCheckboxComponent smallCheckbox(Component label) {\n        return new SmallCheckboxComponent(label);\n    }\n\n    public static SpacerComponent spacer(int percent) {\n        return new SpacerComponent(percent);\n    }\n\n    public static SpacerComponent spacer() {\n        return spacer(100);\n    }\n\n    // -------\n    // Utility\n    // -------\n\n    public static <T, C extends UIComponent> FlowLayout list(List<T> data, Consumer<FlowLayout> layoutConfigurator, Function<T, C> componentMaker, boolean vertical) {\n        var layout = vertical ? UIContainers.verticalFlow(Sizing.content(), Sizing.content()) : UIContainers.horizontalFlow(Sizing.content(), Sizing.content());\n        layoutConfigurator.accept(layout);\n\n        for (var value : data) {\n            layout.child(componentMaker.apply(value));\n        }\n\n        return layout;\n    }\n\n    public static VanillaWidgetComponent wrapVanillaWidget(AbstractWidget widget) {\n        return new VanillaWidgetComponent(widget);\n    }\n\n    public static <T extends UIComponent> T createWithSizing(Supplier<T> componentMaker, Sizing horizontalSizing, Sizing verticalSizing) {\n        var component = componentMaker.get();\n        component.sizing(horizontalSizing, verticalSizing);\n        return component;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/component/VanillaWidgetComponent.java",
    "content": "package io.wispforest.owo.ui.component;\n\nimport io.wispforest.owo.mixin.ui.access.AbstractWidgetAccessor;\nimport io.wispforest.owo.mixin.ui.access.EditBoxAccessor;\nimport io.wispforest.owo.ui.base.BaseUIComponent;\nimport io.wispforest.owo.ui.core.*;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.Checkbox;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.util.Mth;\n\nimport java.util.function.Consumer;\n\npublic class VanillaWidgetComponent extends BaseUIComponent {\n\n    private final AbstractWidget widget;\n\n    protected VanillaWidgetComponent(AbstractWidget widget) {\n        this.widget = widget;\n\n        this.horizontalSizing.set(Sizing.fixed(this.widget.getWidth()));\n        this.verticalSizing.set(Sizing.fixed(this.widget.getHeight()));\n\n        if (widget instanceof EditBox) {\n            this.margins(Insets.none());\n        }\n    }\n\n    public boolean hovered() {\n        return this.hovered;\n    }\n\n    @Override\n    public void mount(ParentUIComponent parent, int x, int y) {\n        super.mount(parent, x, y);\n        this.applyToWidget();\n    }\n\n    @Override\n    protected void updateHoveredState(int mouseX, int mouseY, boolean nowHovered) {\n        this.hovered = nowHovered;\n\n        if (nowHovered) {\n            if (this.root() == null || this.root().childAt(mouseX, mouseY) != this.widget) {\n                this.hovered = false;\n                return;\n            }\n\n            this.mouseEnterEvents.sink().onMouseEnter();\n        } else {\n            this.mouseLeaveEvents.sink().onMouseLeave();\n        }\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        if (this.widget instanceof Button || this.widget instanceof Checkbox || this.widget instanceof SliderComponent) {\n            return 20;\n        } else if (this.widget instanceof EditBox textField) {\n            if (((EditBoxAccessor) textField).owo$bordered()) {\n                return 20;\n            } else {\n                return 9;\n            }\n        } else if (this.widget instanceof TextAreaComponent textArea && textArea.maxLines() > 0) {\n            return Mth.clamp(textArea.getInnerHeight() / 9 + 1, 2, textArea.maxLines()) * 9 + (textArea.displayCharCount() ? 9 + 12 : 9);\n        } else {\n            throw new UnsupportedOperationException(this.widget.getClass().getSimpleName() + \" does not support Sizing.content() on the vertical axis\");\n        }\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        if (this.widget instanceof Button button) {\n            return Minecraft.getInstance().font.width(button.getMessage()) + 8;\n        } else if (this.widget instanceof Checkbox checkbox) {\n            return Minecraft.getInstance().font.width(checkbox.getMessage()) + 24;\n        } else {\n            throw new UnsupportedOperationException(this.widget.getClass().getSimpleName() + \" does not support Sizing.content() on the horizontal axis\");\n        }\n    }\n\n    @Override\n    public BaseUIComponent margins(Insets margins) {\n        if (widget instanceof EditBox) {\n            return super.margins(margins.add(1, 1, 1, 1));\n        } else {\n            return super.margins(margins);\n        }\n    }\n\n    @Override\n    public void inflate(Size space) {\n        super.inflate(space);\n        this.applyToWidget();\n    }\n\n    @Override\n    public void updateX(int x) {\n        super.updateX(x);\n        this.applyToWidget();\n    }\n\n    @Override\n    public void updateY(int y) {\n        super.updateY(y);\n        this.applyToWidget();\n    }\n\n    private void applyToWidget() {\n        var accessor = (AbstractWidgetAccessor) this.widget;\n\n        accessor.owo$setX(this.x + this.widget.xOffset());\n        accessor.owo$setY(this.y + this.widget.yOffset());\n\n        accessor.owo$setWidth(this.width + this.widget.widthOffset());\n        accessor.owo$setHeight(this.height + this.widget.heightOffset());\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public <C extends UIComponent> C configure(Consumer<C> closure) {\n        try {\n            this.runAndDeferEvents(() -> closure.accept((C) this.widget));\n        } catch (ClassCastException theUserDidBadItWasNotMyFault) {\n            throw new IllegalArgumentException(\n                    \"Invalid target class passed when configuring component of type \" + this.getClass().getSimpleName(),\n                    theUserDidBadItWasNotMyFault\n            );\n        }\n\n        return (C) this.widget;\n    }\n\n    @Override\n    public void notifyParentIfMounted() {\n        super.notifyParentIfMounted();\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        this.widget.render(graphics, mouseX, mouseY, 0);\n    }\n\n    @Override\n    public boolean shouldDrawTooltip(double mouseX, double mouseY) {\n        return this.widget.visible && this.widget.active && super.shouldDrawTooltip(mouseX, mouseY);\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        return this.widget.mouseClicked(new MouseButtonEvent(this.x + click.x(), this.y + click.y(), click.buttonInfo()), doubled)\n                | super.onMouseDown(click, doubled);\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        return this.widget.mouseReleased(new MouseButtonEvent(this.x + click.x(), this.y + click.y(), click.buttonInfo()))\n                | super.onMouseUp(click);\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        return this.widget.mouseScrolled(this.x + mouseX, this.y + mouseY, 0, amount)\n                | super.onMouseScroll(mouseX, mouseY, amount);\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.widget.mouseDragged(new MouseButtonEvent(this.x + click.x(), this.y + click.y(), click.buttonInfo()), deltaX, deltaY)\n                | super.onMouseDrag(click, deltaX, deltaY);\n    }\n\n    @Override\n    public boolean onCharTyped(CharacterEvent input) {\n        return this.widget.charTyped(input)\n                | super.onCharTyped(input);\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        return this.widget.keyPressed(input)\n                | super.onKeyPress(input);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/CollapsibleContainer.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.component.LabelComponent;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.Delta;\nimport io.wispforest.owo.ui.util.UISounds;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport org.w3c.dom.Element;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class CollapsibleContainer extends FlowLayout {\n\n    public static final Surface SURFACE = (context, component) -> context.fill(\n            component.x() + 5,\n            component.y(),\n            component.x() + 6,\n            component.y() + component.height(),\n            0x77FFFFFF\n    );\n\n    protected final EventStream<OnToggled> toggledEvents = OnToggled.newStream();\n\n    protected final List<UIComponent> collapsibleChildren = new ArrayList<>();\n    protected final List<UIComponent> collapsibleChildrenView = Collections.unmodifiableList(this.collapsibleChildren);\n    protected boolean expanded;\n\n    protected final SpinnyBoiComponent spinnyBoi;\n    protected final FlowLayout titleLayout;\n    protected final FlowLayout contentLayout;\n\n    protected CollapsibleContainer(Sizing horizontalSizing, Sizing verticalSizing, Component title, boolean expanded) {\n        super(horizontalSizing, verticalSizing, Algorithm.VERTICAL);\n\n        // Title\n\n        this.titleLayout = UIContainers.horizontalFlow(Sizing.content(), Sizing.content());\n        this.titleLayout.padding(Insets.of(5, 5, 5, 0));\n        this.allowOverflow(true);\n\n        title = title.copy().withStyle(ChatFormatting.UNDERLINE);\n        this.titleLayout.child(UIComponents.label(title).cursorStyle(CursorStyle.HAND));\n\n        this.spinnyBoi = new SpinnyBoiComponent();\n        this.titleLayout.child(spinnyBoi);\n\n        this.expanded = expanded;\n        this.spinnyBoi.targetRotation = expanded ? 90 : 0;\n        this.spinnyBoi.rotation = this.spinnyBoi.targetRotation;\n\n        super.child(this.titleLayout);\n\n        // Content\n\n        this.contentLayout = UIContainers.verticalFlow(Sizing.content(), Sizing.content());\n        this.contentLayout.padding(Insets.left(15));\n        this.contentLayout.surface(SURFACE);\n\n        super.child(this.contentLayout);\n    }\n\n    public FlowLayout titleLayout() {\n        return this.titleLayout;\n    }\n\n    public List<UIComponent> collapsibleChildren() {\n        return this.collapsibleChildrenView;\n    }\n\n    public boolean expanded() {\n        return this.expanded;\n    }\n\n    public EventSource<OnToggled> onToggled() {\n        return this.toggledEvents.source();\n    }\n\n    public void toggleExpansion() {\n        if (expanded) {\n            this.contentLayout.clearChildren();\n            this.spinnyBoi.targetRotation = 0;\n        } else {\n            this.contentLayout.children(this.collapsibleChildren);\n            this.spinnyBoi.targetRotation = 90;\n        }\n\n        this.expanded = !this.expanded;\n        this.toggledEvents.sink().onToggle(this.expanded);\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return source == FocusSource.KEYBOARD_CYCLE;\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        if (input.isSelection()) {\n            this.toggleExpansion();\n\n            super.onKeyPress(input);\n            return true;\n        }\n\n        return super.onKeyPress(input);\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        final var superResult = super.onMouseDown(click, doubled);\n\n        if (click.y() <= this.titleLayout.fullSize().height() && !superResult) {\n            this.toggleExpansion();\n            UISounds.playInteractionSound();\n            return true;\n        } else {\n            return superResult;\n        }\n    }\n\n    @Override\n    public FlowLayout child(UIComponent child) {\n        this.collapsibleChildren.add(child);\n        if (this.expanded) this.contentLayout.child(child);\n        return this;\n    }\n\n    @Override\n    public FlowLayout children(Collection<? extends UIComponent> children) {\n        this.collapsibleChildren.addAll(children);\n        if (this.expanded) this.contentLayout.children(children);\n        return this;\n    }\n\n    @Override\n    public FlowLayout child(int index, UIComponent child) {\n        this.collapsibleChildren.add(index, child);\n        if (this.expanded) this.contentLayout.child(index, child);\n        return this;\n    }\n\n    @Override\n    public FlowLayout children(int index, Collection<? extends UIComponent> children) {\n        this.collapsibleChildren.addAll(index, children);\n        if (this.expanded) this.contentLayout.children(index, children);\n        return this;\n    }\n\n    @Override\n    public FlowLayout removeChild(UIComponent child) {\n        this.collapsibleChildren.remove(child);\n        return this.contentLayout.removeChild(child);\n    }\n\n    public static CollapsibleContainer parse(Element element) {\n        var textElement = UIParsing.childElements(element).get(\"text\");\n        var title = textElement == null ? Component.empty() : UIParsing.parseText(textElement);\n\n        return element.getAttribute(\"expanded\").equals(\"true\")\n                ? UIContainers.collapsible(Sizing.content(), Sizing.content(), title, true)\n                : UIContainers.collapsible(Sizing.content(), Sizing.content(), title, false);\n    }\n\n    public interface OnToggled {\n        void onToggle(boolean nowExpanded);\n\n        static EventStream<OnToggled> newStream() {\n            return new EventStream<>(subscribers -> nowExpanded -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onToggle(nowExpanded);\n                }\n            });\n        }\n    }\n\n    protected static class SpinnyBoiComponent extends LabelComponent {\n\n        protected float rotation = 90;\n        protected float targetRotation = 90;\n\n        public SpinnyBoiComponent() {\n            super(Component.literal(\">\"));\n            this.margins(Insets.of(0, 0, 5, 10));\n            this.cursorStyle(CursorStyle.HAND);\n        }\n\n        @Override\n        public void update(float delta, int mouseX, int mouseY) {\n            super.update(delta, mouseX, mouseY);\n            this.rotation += Delta.compute(this.rotation, this.targetRotation, delta * .65);\n        }\n\n        @Override\n        public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n            var matrices = graphics.pose();\n\n            matrices.pushMatrix();\n            matrices.translate(this.x + this.width / 2f - 1, this.y + this.height / 2f - 1);\n            matrices.rotate((float) Math.toRadians(this.rotation));\n            matrices.translate(-(this.x + this.width / 2f - 1), -(this.y + this.height / 2f - 1));\n\n            super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n            matrices.popMatrix();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/DraggableContainer.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\n\nimport java.util.Map;\n\npublic class DraggableContainer<C extends UIComponent> extends WrappingParentUIComponent<C> {\n\n    protected int foreheadSize = 10;\n\n    protected int baseX = 0, baseY = 0;\n    protected double xOffset = 0, yOffset = 0;\n\n    protected DraggableContainer(Sizing horizontalSizing, Sizing verticalSizing, C child) {\n        super(horizontalSizing, verticalSizing, child);\n        this.padding(Insets.none());\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n        this.drawChildren(graphics, mouseX, mouseY, partialTicks, delta, this.childView);\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return source == FocusSource.MOUSE_CLICK;\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        this.xOffset += deltaX;\n        this.yOffset += deltaY;\n\n        super.updateX((int) (this.baseX + Math.round(this.xOffset)));\n        super.updateY((int) (this.baseY + Math.round(this.yOffset)));\n        return super.onMouseDrag(click, deltaX, deltaY);\n    }\n\n    @Override\n    public @Nullable UIComponent childAt(int x, int y) {\n        if (this.isInBoundingBox(x, y) && y - this.y < this.foreheadSize) {\n            return this;\n        }\n\n        return super.childAt(x, y);\n    }\n\n    @Override\n    public void updateX(int x) {\n        this.baseX = x;\n        super.updateX((int) (x + Math.round(this.xOffset)));\n    }\n\n    @Override\n    public void updateY(int y) {\n        this.baseY = y;\n        super.updateY((int) (y + Math.round(this.yOffset)));\n    }\n\n    @Override\n    public int baseX() {\n        return this.baseX;\n    }\n\n    @Override\n    public int baseY() {\n        return this.baseY;\n    }\n\n    @Override\n    public ParentUIComponent padding(Insets padding) {\n        return super.padding(Insets.of(padding.top() + this.foreheadSize, padding.bottom(), padding.left(), padding.right()));\n    }\n\n    public DraggableContainer<C> foreheadSize(int foreheadSize) {\n        int prevForeheadSize = this.foreheadSize;\n        this.foreheadSize = foreheadSize;\n\n        var padding = this.padding.get();\n        this.padding(Insets.of(padding.top() - prevForeheadSize, padding.bottom(), padding.left(), padding.right()));\n        return this;\n    }\n\n    public int foreheadSize() {\n        return this.foreheadSize;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"forehead-size\", UIParsing::parseUnsignedInt, this::foreheadSize);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/FlowLayout.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.base.BaseParentUIComponent;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.MountingHelper;\nimport io.wispforest.owo.util.Observable;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport org.apache.commons.lang3.mutable.MutableInt;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.*;\n\npublic class FlowLayout extends BaseParentUIComponent {\n\n    protected final List<UIComponent> children = new ArrayList<>();\n    protected final List<UIComponent> childrenView = Collections.unmodifiableList(this.children);\n    protected final Algorithm algorithm;\n\n    protected Size contentSize = Size.zero();\n    protected Observable<Integer> gap = Observable.of(0);\n\n    protected FlowLayout(Sizing horizontalSizing, Sizing verticalSizing, Algorithm algorithm) {\n        super(horizontalSizing, verticalSizing);\n        this.algorithm = algorithm;\n\n        this.gap.observe(integer -> this.updateLayout());\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.contentSize.width() + this.padding.get().horizontal();\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return this.contentSize.height() + this.padding.get().vertical();\n    }\n\n    @Override\n    public void layout(Size space) {\n        this.algorithm.layout(this);\n    }\n\n    /**\n     * Add a single child to this layout. If you need to add multiple\n     * children, use {@link #children(Collection)} instead\n     *\n     * @param child The child to append to this layout\n     */\n    public FlowLayout child(UIComponent child) {\n        this.children.add(child);\n        this.updateLayout();\n        return this;\n    }\n\n    /**\n     * Add a collection of children to this layout. If you only need to\n     * add a single child to, use {@link #child(UIComponent)} instead\n     *\n     * @param children The children to add to this layout\n     */\n    public FlowLayout children(Collection<? extends UIComponent> children) {\n        this.children.addAll(children);\n        this.updateLayout();\n        return this;\n    }\n\n    /**\n     * Insert a single child into this layout. If you need to insert multiple\n     * children, use {@link #children(int, Collection)} instead\n     *\n     * @param index The index at which to insert the child\n     * @param child The child to append to this layout\n     */\n    public FlowLayout child(int index, UIComponent child) {\n        this.children.add(index, child);\n        this.updateLayout();\n        return this;\n    }\n\n    /**\n     * Insert a collection of children into this layout. If you only need to\n     * insert a single child to, use {@link #child(int, UIComponent)} instead\n     *\n     * @param index    The index at which to begin inserting children\n     * @param children The children to add to this layout\n     */\n    public FlowLayout children(int index, Collection<? extends UIComponent> children) {\n        this.children.addAll(index, children);\n        this.updateLayout();\n        return this;\n    }\n\n    @Override\n    public FlowLayout removeChild(UIComponent child) {\n        if (this.children.remove(child)) {\n            child.dismount(DismountReason.REMOVED);\n            this.updateLayout();\n        }\n\n        return this;\n    }\n\n    /**\n     * Remove all children from this layout\n     */\n    public FlowLayout clearChildren() {\n        for (var child : this.children) {\n            child.dismount(DismountReason.REMOVED);\n        }\n\n        this.children.clear();\n        this.updateLayout();\n\n        return this;\n    }\n\n    @Override\n    public List<UIComponent> children() {\n        return this.childrenView;\n    }\n\n    /**\n     * Set the gap, in logical pixels, this layout\n     * should insert between all child components\n     */\n    public FlowLayout gap(int gap) {\n        this.gap.set(gap);\n        return this;\n    }\n\n    /**\n     * @return The gap, in logical pixels, this layout\n     * inserts between all child components\n     */\n    public int gap() {\n        return this.gap.get();\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n        this.drawChildren(graphics, mouseX, mouseY, partialTicks, delta, this.children);\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        UIParsing.apply(children, \"gap\", UIParsing::parseSignedInt, this::gap);\n\n        final var components = UIParsing\n                .get(children, \"children\", e -> UIParsing.<Element>allChildrenOfType(e, Node.ELEMENT_NODE))\n                .orElse(Collections.emptyList());\n\n        for (var child : components) {\n            this.child(model.parseComponent(UIComponent.class, child));\n        }\n    }\n\n    @Override\n    public MutableComponent inspectorDescriptor() {\n        final var descriptor = super.inspectorDescriptor();\n        return this.gap() == 0 ? descriptor : descriptor.append(\n                Component.literal(\" [\" + this.gap() + \"]\")\n        );\n    }\n\n    public static FlowLayout parse(Element element) {\n        UIParsing.expectAttributes(element, \"direction\");\n\n        return switch (element.getAttribute(\"direction\")) {\n            case \"horizontal\" -> UIContainers.horizontalFlow(Sizing.content(), Sizing.content());\n            case \"ltr-text-flow\" -> UIContainers.ltrTextFlow(Sizing.content(), Sizing.content());\n            default -> UIContainers.verticalFlow(Sizing.content(), Sizing.content());\n        };\n    }\n\n    @FunctionalInterface\n    public interface Algorithm {\n        void layout(FlowLayout container);\n\n        Algorithm HORIZONTAL = container -> {\n            var layoutWidth = new MutableInt(0);\n            var layoutHeight = new MutableInt(0);\n\n            final var layout = new ArrayList<UIComponent>();\n            final var padding = container.padding.get();\n            final var childSpace = container.calculateChildSpace(container.space);\n\n            MountingHelper.inflateWithExpand(container.children, childSpace, false, container.gap());\n            var mountState = MountingHelper.mountEarly(container::mountChild, container.children, child -> {\n                layout.add(child);\n\n                child.mount(container,\n                        container.x + padding.left() + child.margins().get().left() + layoutWidth.intValue(),\n                        container.y + padding.top() + child.margins().get().top());\n\n                final var childSize = child.fullSize();\n                layoutWidth.add(childSize.width() + container.gap());\n                if (childSize.height() > layoutHeight.intValue()) {\n                    layoutHeight.setValue(childSize.height());\n                }\n            });\n\n            layoutWidth.subtract(container.gap());\n\n            container.contentSize = Size.of(layoutWidth.intValue(), layoutHeight.intValue());\n            container.applySizing();\n\n            if (container.verticalAlignment() != VerticalAlignment.TOP) {\n                for (var component : layout) {\n                    component.updateY(component.baseY() + container.verticalAlignment().align(component.fullSize().height(), container.height - padding.vertical()));\n                }\n            }\n\n            if (container.horizontalAlignment() != HorizontalAlignment.LEFT) {\n                for (var component : layout) {\n                    if (container.horizontalAlignment() == HorizontalAlignment.CENTER) {\n                        component.updateX(component.baseX() + (container.width - padding.horizontal() - layoutWidth.intValue()) / 2);\n                    } else {\n                        component.updateX(component.baseX() + (container.width - padding.horizontal() - layoutWidth.intValue()));\n                    }\n                }\n            }\n\n            mountState.mountLate();\n        };\n\n        Algorithm VERTICAL = container -> {\n            var layoutHeight = new MutableInt(0);\n            var layoutWidth = new MutableInt(0);\n\n            final var layout = new ArrayList<UIComponent>();\n            final var padding = container.padding.get();\n            final var childSpace = container.calculateChildSpace(container.space);\n\n            MountingHelper.inflateWithExpand(container.children, childSpace, true, container.gap());\n            var mountState = MountingHelper.mountEarly(container::mountChild, container.children, child -> {\n                layout.add(child);\n\n                child.mount(container,\n                        container.x + padding.left() + child.margins().get().left(),\n                        container.y + padding.top() + child.margins().get().top() + layoutHeight.intValue());\n\n                final var childSize = child.fullSize();\n                layoutHeight.add(childSize.height() + container.gap());\n                if (childSize.width() > layoutWidth.intValue()) {\n                    layoutWidth.setValue(childSize.width());\n                }\n            });\n\n            layoutHeight.subtract(container.gap());\n\n            container.contentSize = Size.of(layoutWidth.intValue(), layoutHeight.intValue());\n            container.applySizing();\n\n            if (container.horizontalAlignment() != HorizontalAlignment.LEFT) {\n                for (var component : layout) {\n                    component.updateX(component.baseX() + container.horizontalAlignment().align(component.fullSize().width(), container.width - padding.horizontal()));\n                }\n            }\n\n            if (container.verticalAlignment() != VerticalAlignment.TOP) {\n                for (var component : layout) {\n                    if (container.verticalAlignment() == VerticalAlignment.CENTER) {\n                        component.updateY(component.baseY() + (container.height - padding.vertical() - layoutHeight.intValue()) / 2);\n                    } else {\n                        component.updateY(component.baseY() + (container.height - padding.vertical() - layoutHeight.intValue()));\n                    }\n                }\n            }\n\n            mountState.mountLate();\n        };\n\n        Algorithm LTR_TEXT = container -> {\n            if (container.horizontalSizing.get().isContent()) {\n                throw new IllegalStateException(\"An LTR-text-flow layout must use content-independent horizontal sizing\");\n            }\n\n            var layoutWidth = new MutableInt(0);\n            var layoutHeight = new MutableInt(0);\n\n            var rowWidth = new MutableInt(0);\n            var rowOffset = new MutableInt(0);\n\n            final var layout = new ArrayList<UIComponent>();\n            final var padding = container.padding.get();\n            final var childSpace = container.calculateChildSpace(container.space);\n\n            container.children.forEach(child -> child.inflate(childSpace));\n\n            var mountState = MountingHelper.mountEarly(container::mountChild, container.children, child -> {\n                layout.add(child);\n\n                int x = container.x + padding.left() + child.margins().get().left() + rowWidth.intValue();\n                int y = container.y + padding.top() + child.margins().get().top() + rowOffset.intValue();\n\n                final var childSize = child.fullSize();\n                if (rowWidth.intValue() + childSize.width() > childSpace.width()) {\n                    x -= rowWidth.intValue();\n                    y = y - rowOffset.intValue() + layoutHeight.intValue();\n\n                    rowOffset.setValue(layoutHeight);\n                    rowWidth.setValue(0);\n                }\n\n                child.mount(container, x, y);\n\n                rowWidth.add(childSize.width() + container.gap());\n                if (rowOffset.intValue() + childSize.height() > layoutHeight.intValue()) {\n                    layoutHeight.setValue(rowOffset.intValue() + childSize.height());\n                }\n                if (rowWidth.intValue() > layoutWidth.intValue()) {\n                    layoutWidth.setValue(rowWidth.intValue());\n                }\n            });\n\n            layoutWidth.subtract(container.gap());\n\n            container.contentSize = Size.of(layoutWidth.intValue(), layoutHeight.intValue());\n            container.applySizing();\n\n            if (container.verticalAlignment() != VerticalAlignment.TOP) {\n                for (var component : layout) {\n                    component.updateY(component.baseY() + container.verticalAlignment().align(layoutHeight.intValue(), container.height - padding.vertical()));\n                }\n            }\n\n            if (container.horizontalAlignment() != HorizontalAlignment.LEFT) {\n                for (var component : layout) {\n                    if (container.horizontalAlignment() == HorizontalAlignment.CENTER) {\n                        component.updateX(component.baseX() + (container.width - padding.horizontal() - layoutWidth.intValue()) / 2);\n                    } else {\n                        component.updateX(component.baseX() + (container.width - padding.horizontal() - layoutWidth.intValue()));\n                    }\n                }\n            }\n\n            mountState.mountLate();\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/GridLayout.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.base.BaseParentUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Size;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport org.apache.commons.lang3.mutable.MutableInt;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.*;\n\npublic class GridLayout extends BaseParentUIComponent {\n\n    protected final int rows, columns;\n\n    protected final UIComponent[] children;\n    protected final List<UIComponent> nonNullChildren = new ArrayList<>();\n    protected final List<UIComponent> nonNullChildrenView = Collections.unmodifiableList(this.nonNullChildren);\n\n    protected Size contentSize = Size.zero();\n\n    protected GridLayout(Sizing horizontalSizing, Sizing verticalSizing, int rows, int columns) {\n        super(horizontalSizing, verticalSizing);\n\n        this.rows = rows;\n        this.columns = columns;\n\n        this.children = new UIComponent[rows * columns];\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.contentSize.width() + this.padding.get().right();\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return this.contentSize.height() + this.padding.get().bottom();\n    }\n\n    @Override\n    public void layout(Size space) {\n        int[] columnSizes = new int[this.columns];\n        int[] rowSizes = new int[this.rows];\n\n        var childSpace = this.calculateChildSpace(space);\n        for (var child : this.children) {\n            if (child != null) {\n                child.inflate(childSpace);\n            }\n        }\n\n        this.determineSizes(columnSizes, false);\n        this.determineSizes(rowSizes, true);\n\n        var mountingOffset = this.childMountingOffset();\n        var layoutX = new MutableInt(this.x + mountingOffset.width());\n        var layoutY = new MutableInt(this.y + mountingOffset.height());\n\n        for (int row = 0; row < this.rows; row++) {\n            layoutX.setValue(this.x + mountingOffset.width());\n\n            for (int column = 0; column < this.columns; column++) {\n                int columnSize = columnSizes[column];\n                int rowSize = rowSizes[row];\n\n                this.mountChild(this.getChild(row, column), child -> {\n                    child.mount(\n                            this,\n                            layoutX.intValue() + child.margins().get().left() + this.horizontalAlignment().align(child.fullSize().width(), columnSize),\n                            layoutY.intValue() + child.margins().get().top() + this.verticalAlignment().align(child.fullSize().height(), rowSize)\n                    );\n                });\n\n\n                layoutX.add(columnSizes[column]);\n            }\n\n            layoutY.add(rowSizes[row]);\n        }\n\n        this.contentSize = Size.of(layoutX.intValue() - this.x, layoutY.intValue() - this.y);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n        this.drawChildren(graphics, mouseX, mouseY, partialTicks, delta, this.nonNullChildren);\n    }\n\n    protected @Nullable UIComponent getChild(int row, int column) {\n        return this.children[row * this.columns + column];\n    }\n\n    protected void determineSizes(int[] sizes, boolean rows) {\n        if (!(rows ? this.verticalSizing : this.horizontalSizing).get().isContent()) {\n            Arrays.fill(sizes, (rows ? this.height - this.padding().get().vertical() : this.width - this.padding().get().horizontal()) / (rows ? this.rows : this.columns));\n        } else {\n            for (int row = 0; row < this.rows; row++) {\n                for (int column = 0; column < this.columns; column++) {\n                    final var child = this.getChild(row, column);\n                    if (child == null) continue;\n\n                    if (rows) {\n                        sizes[row] = Math.max(sizes[row], child.fullSize().height());\n                    } else {\n                        sizes[column] = Math.max(sizes[column], child.fullSize().width());\n                    }\n                }\n            }\n        }\n    }\n\n    public GridLayout child(UIComponent child, int row, int column) {\n        var previousChild = this.getChild(row, column);\n        this.children[row * this.columns + column] = child;\n\n        if (previousChild != child) {\n            if (previousChild != null) {\n                this.nonNullChildren.remove(previousChild);\n                previousChild.dismount(DismountReason.REMOVED);\n            }\n\n            this.nonNullChildren.add(child);\n            this.updateLayout();\n        }\n\n        return this;\n    }\n\n    public GridLayout removeChild(int row, int column) {\n        var currentChild = getChild(row, column);\n        if (currentChild != null) {\n            currentChild.dismount(DismountReason.REMOVED);\n\n            this.nonNullChildren.remove(currentChild);\n            this.updateLayout();\n        }\n\n        return this;\n    }\n\n    @Override\n    public GridLayout removeChild(UIComponent child) {\n        for (int i = 0; i < this.children.length; i++) {\n            if (Objects.equals(this.children[i], child)) {\n                this.removeChild(i / this.columns, i % columns);\n                break;\n            }\n        }\n\n        return this;\n    }\n\n    @Override\n    public List<UIComponent> children() {\n        return this.nonNullChildrenView;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        final var components = UIParsing\n                .get(children, \"children\", e -> UIParsing.<Element>allChildrenOfType(e, Node.ELEMENT_NODE))\n                .orElse(Collections.emptyList());\n\n        for (var child : components) {\n            UIParsing.expectAttributes(child, \"row\", \"column\");\n\n            int row = UIParsing.parseUnsignedInt(child.getAttributeNode(\"row\"));\n            int column = UIParsing.parseUnsignedInt(child.getAttributeNode(\"column\"));\n\n            final var existingChild = this.getChild(row, column);\n            if (existingChild != null) {\n                throw new UIModelParsingException(\"Tried to populate cell \" + row + \",\" + column + \" in grid layout twice. \" +\n                        \"Present component: \" + existingChild.getClass().getSimpleName() + \"\\nNew element: \" + child.getNodeName());\n            }\n\n            this.child(model.parseComponent(UIComponent.class, child), row, column);\n        }\n    }\n\n    public static GridLayout parse(Element element) {\n        UIParsing.expectAttributes(element, \"rows\", \"columns\");\n\n        int rows = UIParsing.parseUnsignedInt(element.getAttributeNode(\"rows\"));\n        int columns = UIParsing.parseUnsignedInt(element.getAttributeNode(\"columns\"));\n\n        return new GridLayout(Sizing.content(), Sizing.content(), rows, columns);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/OverlayContainer.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.util.EventSource;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.jetbrains.annotations.Nullable;\n\npublic class OverlayContainer<C extends UIComponent> extends WrappingParentUIComponent<C> {\n\n    protected boolean closeOnClick = true;\n    protected @Nullable EventSource<?>.Subscription exitSubscription = null;\n\n    protected OverlayContainer(C child) {\n        super(Sizing.fill(100), Sizing.fill(100), child);\n\n        this.positioning(Positioning.absolute(0, 0));\n        this.surface(Surface.VANILLA_TRANSLUCENT);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n        this.drawChildren(graphics, mouseX, mouseY, partialTicks, delta, this.childView);\n    }\n\n    @Override\n    public void drawFocusHighlight(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {}\n\n    @Override\n    public void mount(ParentUIComponent parent, int x, int y) {\n        super.mount(parent, x, y);\n        this.exitSubscription = this.root().keyPress().subscribe((input) -> {\n            if (input.isEscape()) {\n                this.remove();\n                return true;\n            }\n\n            return false;\n        });\n    }\n\n    @Override\n    public void dismount(DismountReason reason) {\n        super.dismount(reason);\n\n        if (this.exitSubscription != null) {\n            this.exitSubscription.cancel();\n        }\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        boolean handled = super.onMouseDown(click, doubled) || this.child.isInBoundingBox(click.x(), click.y());\n\n        if (!handled && this.closeOnClick) {\n            this.remove();\n            return true;\n        } else {\n            return handled;\n        }\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        super.onMouseScroll(mouseX, mouseY, amount);\n        return true;\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return source == FocusSource.KEYBOARD_CYCLE;\n    }\n\n    @Override\n    protected int childMountX() {\n        return this.x + this.padding.get().left() + (this.width - this.child.fullSize().width()) / 2;\n    }\n\n    @Override\n    protected int childMountY() {\n        return this.y + this.padding.get().top() + (this.height() - this.child.fullSize().height()) / 2;\n    }\n\n    /**\n     * Set whether this overlay should close when a mouse\n     * click occurs outside the bounds of its contents\n     */\n    public OverlayContainer<C> closeOnClick(boolean closeOnClick) {\n        this.closeOnClick = closeOnClick;\n        return this;\n    }\n\n    /**\n     * Whether this overlay should close when a mouse\n     * click occurs outside the bounds of its contents\n     */\n    public boolean closeOnClick() {\n        return closeOnClick;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/ScrollContainer.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.Delta;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.Nullable;\nimport org.jetbrains.annotations.Range;\nimport org.lwjgl.glfw.GLFW;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\n\npublic class ScrollContainer<C extends UIComponent> extends WrappingParentUIComponent<C> {\n\n    public static final Identifier VERTICAL_VANILLA_SCROLLBAR_TEXTURE = Owo.id(\"scrollbar/vanilla_vertical\");\n    public static final Identifier DISABLED_VERTICAL_VANILLA_SCROLLBAR_TEXTURE = Owo.id(\"scrollbar/vanilla_vertical_disabled\");\n    public static final Identifier HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE = Owo.id(\"scrollbar/vanilla_horizontal_disabled\");\n    public static final Identifier DISABLED_HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE = Owo.id(\"scrollbar/vanilla_horizontal_disabled\");\n    public static final Identifier VANILLA_SCROLLBAR_TRACK_TEXTURE = Owo.id(\"scrollbar/track\");\n    public static final Identifier FLAT_VANILLA_SCROLLBAR_TEXTURE = Owo.id(\"scrollbar/vanilla_flat\");\n\n    protected double scrollOffset = 0;\n    protected double currentScrollPosition = 0;\n    protected int lastScrollPosition = -1;\n    protected int scrollStep = 0;\n\n    protected int fixedScrollbarLength = 0;\n    protected double lastScrollbarLength = 0;\n\n    protected Scrollbar scrollbar = Scrollbar.flat(Color.ofArgb(0xA0000000));\n    protected int scrollbarThiccness = 3;\n\n    protected long lastScrollbarInteractTime = 0;\n    protected int scrollbarOffset = 0;\n    protected boolean scrollbaring = false;\n\n    protected int maxScroll = 0;\n    protected int childSize = 0;\n\n    protected final ScrollDirection direction;\n\n    protected ScrollContainer(ScrollDirection direction, Sizing horizontalSizing, Sizing verticalSizing, C child) {\n        super(horizontalSizing, verticalSizing, child);\n        this.direction = direction;\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        if (this.direction == ScrollDirection.VERTICAL) {\n            return super.determineHorizontalContentSize(sizing);\n        } else {\n            throw new UnsupportedOperationException(\"Horizontal ScrollContainer cannot be horizontally content-sized\");\n        }\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        if (this.direction == ScrollDirection.HORIZONTAL) {\n            return super.determineVerticalContentSize(sizing);\n        } else {\n            throw new UnsupportedOperationException(\"Vertical ScrollContainer cannot be vertically content-sized\");\n        }\n    }\n\n    @Override\n    public void layout(Size space) {\n        super.layout(space);\n\n        this.maxScroll = Math.max(0, this.direction.sizeGetter.apply(child) - (this.direction.sizeGetter.apply(this) - this.direction.insetGetter.apply(this.padding.get())));\n        this.scrollOffset = Mth.clamp(this.scrollOffset, 0, this.maxScroll + .5);\n        this.childSize = this.direction.sizeGetter.apply(this.child);\n        this.lastScrollPosition = -1;\n    }\n\n    @Override\n    protected int childMountX() {\n        return (int) (super.childMountX() - this.direction.choose(this.currentScrollPosition, 0));\n    }\n\n    @Override\n    protected int childMountY() {\n        return (int) (super.childMountY() - this.direction.choose(0, this.currentScrollPosition));\n    }\n\n    @Override\n    protected void parentUpdate(float delta, int mouseX, int mouseY) {\n        super.parentUpdate(delta, mouseX, mouseY);\n        this.currentScrollPosition += Delta.compute(this.currentScrollPosition, this.scrollOffset, delta * .5);\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n\n        // Update child\n        int effectiveScrollOffset = this.scrollStep > 0\n                ? ((int) this.scrollOffset / this.scrollStep) * this.scrollStep\n                : (int) this.currentScrollPosition;\n        if (this.scrollStep > 0 && this.maxScroll - this.scrollOffset == -1) {\n            effectiveScrollOffset += this.scrollOffset % this.scrollStep;\n        }\n\n        int newScrollPosition = this.direction.coordinateGetter.apply(this) - effectiveScrollOffset;\n        if (newScrollPosition != this.lastScrollPosition) {\n            this.direction.coordinateSetter.accept(this.child, newScrollPosition + (this.direction == ScrollDirection.VERTICAL\n                    ? this.padding.get().top() + this.child.margins().get().top()\n                    : this.padding.get().left() + this.child.margins().get().left())\n            );\n            this.lastScrollPosition = newScrollPosition;\n        }\n\n        // Draw, adding the fractional part of the offset via matrix translation\n        graphics.pose().pushMatrix();\n\n        double visualOffset = -(this.currentScrollPosition % 1d);\n        if (visualOffset > 9999999e-7 || visualOffset < .1e-6) visualOffset = 0;\n\n        graphics.pose().translate((float) this.direction.choose(visualOffset, 0), (float) this.direction.choose(0, visualOffset));\n        this.drawChildren(graphics, mouseX, mouseY, partialTicks, delta, this.childView);\n\n        graphics.pose().popMatrix();\n\n        // -----\n\n        // Highlight the scrollbar if it's being hovered\n        if (this.isInScrollbar(mouseX, mouseY) || this.scrollbaring) {\n            this.lastScrollbarInteractTime = System.currentTimeMillis() + 1500;\n        }\n\n        var padding = this.padding.get();\n        int selfSize = this.direction.sizeGetter.apply(this);\n        int contentSize = this.direction.sizeGetter.apply(this) - this.direction.insetGetter.apply(padding);\n\n        // Determine the offset of the scrollbar on the\n        // *opposite* axis to the one we scroll on\n        this.scrollbarOffset = this.direction == ScrollDirection.VERTICAL\n                ? this.x + this.width - padding.right() - scrollbarThiccness\n                : this.y + this.height - padding.bottom() - scrollbarThiccness;\n\n        this.lastScrollbarLength = this.fixedScrollbarLength == 0\n                ? Math.min(Math.floor(((float) selfSize / this.childSize) * contentSize), contentSize)\n                : this.fixedScrollbarLength;\n        double scrollbarPosition = this.maxScroll != 0\n                ? (this.currentScrollPosition / this.maxScroll) * (contentSize - this.lastScrollbarLength)\n                : 0;\n\n        if (this.direction == ScrollDirection.VERTICAL) {\n            this.scrollbar.draw(graphics,\n                    this.scrollbarOffset,\n                    (int) (this.y + scrollbarPosition + padding.top()),\n                    this.scrollbarThiccness,\n                    (int) (this.lastScrollbarLength),\n                    this.scrollbarOffset, this.y + padding.top(),\n                    this.scrollbarThiccness, this.height - padding.vertical(),\n                    lastScrollbarInteractTime, this.direction,\n                    this.maxScroll > 0\n            );\n        } else {\n            this.scrollbar.draw(graphics,\n                    (int) (this.x + scrollbarPosition + padding.left()),\n                    this.scrollbarOffset,\n                    (int) (this.lastScrollbarLength),\n                    this.scrollbarThiccness,\n                    this.x + padding.left(), this.scrollbarOffset,\n                    this.width - padding.horizontal(), this.scrollbarThiccness,\n                    lastScrollbarInteractTime, this.direction,\n                    this.maxScroll > 0\n            );\n        }\n    }\n\n    @Override\n    public boolean canFocus(FocusSource source) {\n        return true;\n    }\n\n    @Override\n    public boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        if (this.child.onMouseScroll(this.x + mouseX - this.child.x(), this.y + mouseY - this.child.y(), amount))\n            return true;\n\n        if (this.scrollStep < 1) {\n            this.scrollBy(-amount * 15, false, true);\n        } else {\n            this.scrollBy(-amount * this.scrollStep, true, true);\n        }\n\n        return true;\n    }\n\n    @Override\n    public boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        if (this.isInScrollbar(this.x + click.x(), this.y + click.y())) {\n            super.onMouseDown(click, doubled);\n            return true;\n        } else {\n            return super.onMouseDown(click, doubled);\n        }\n    }\n\n    @Override\n    public boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        if (!this.scrollbaring && !this.isInScrollbar(this.x + click.x(), this.y + click.y()))\n            return super.onMouseDrag(click, deltaX, deltaY);\n\n        double delta = this.direction.choose(deltaX, deltaY);\n        double selfSize = this.direction.sizeGetter.apply(this) - this.direction.insetGetter.apply(this.padding.get());\n        double scalar = (this.maxScroll) / (selfSize - this.lastScrollbarLength);\n        if (!Double.isFinite(scalar)) scalar = 0;\n\n        this.scrollBy(delta * scalar, true, false);\n        this.scrollbaring = true;\n\n        return true;\n    }\n\n    @Override\n    public boolean onKeyPress(KeyEvent input) {\n        if (input.key() == this.direction.lessKeycode) {\n            this.scrollBy(-10, false, true);\n        } else if (input.key() == this.direction.moreKeycode) {\n            this.scrollBy(10, false, true);\n        } else if (input.key() == GLFW.GLFW_KEY_PAGE_DOWN) {\n            this.scrollBy(this.direction.choose(this.width, this.height) * .8, false, true);\n            this.lastScrollbarInteractTime = System.currentTimeMillis() + 1250;\n        } else if (input.key() == GLFW.GLFW_KEY_PAGE_UP) {\n            this.scrollBy(this.direction.choose(this.width, this.height) * -.8, false, true);\n        }\n\n        return false;\n    }\n\n    @Override\n    public boolean onMouseUp(MouseButtonEvent click) {\n        this.scrollbaring = false;\n        return true;\n    }\n\n    @Override\n    public @Nullable UIComponent childAt(int x, int y) {\n        if (this.isInScrollbar(x, y)) {\n            return this;\n        } else {\n            return super.childAt(x, y);\n        }\n    }\n\n    protected void scrollBy(double offset, boolean instant, boolean showScrollbar) {\n        this.scrollOffset = Mth.clamp(this.scrollOffset + offset, 0, this.maxScroll + .5);\n        if (instant) this.currentScrollPosition = this.scrollOffset;\n        if (showScrollbar) this.lastScrollbarInteractTime = System.currentTimeMillis() + 1250;\n    }\n\n    protected boolean isInScrollbar(double mouseX, double mouseY) {\n        return this.isInBoundingBox(mouseX, mouseY) && this.direction.choose(mouseY, mouseX) >= this.scrollbarOffset;\n    }\n\n    /**\n     * Scroll to the given component\n     */\n    public ScrollContainer<C> scrollTo(UIComponent component) {\n        if (this.direction == ScrollDirection.VERTICAL) {\n            this.scrollOffset = Mth.clamp(this.scrollOffset - (this.y - component.y() + component.margins().get().top()), 0, this.maxScroll);\n        } else {\n            this.scrollOffset = Mth.clamp(this.scrollOffset - (this.x - component.x() + component.margins().get().right()), 0, this.maxScroll);\n        }\n        return this;\n    }\n\n    /**\n     * Scroll to the specified point along the entire\n     * length of this container's content\n     */\n    public ScrollContainer<C> scrollTo(@Range(from = 0, to = 1) double progress) {\n        this.scrollOffset = this.maxScroll * progress;\n        return this;\n    }\n\n    /**\n     * Set the thickness of this container's scrollbar,\n     * in logical pixels\n     */\n    public ScrollContainer<C> scrollbarThiccness(int scrollbarThiccness) {\n        this.scrollbarThiccness = scrollbarThiccness;\n        return this;\n    }\n\n    /**\n     * @return The thickness of this container's scrollbar,\n     * in logical pixels\n     */\n    public int scrollbarThiccness() {\n        return this.scrollbarThiccness;\n    }\n\n    /**\n     * Set the scrollbar this container should display. To create one,\n     * look at the static methods on {@link Scrollbar} or use a lambda\n     */\n    public ScrollContainer<C> scrollbar(Scrollbar scrollbar) {\n        this.scrollbar = scrollbar;\n        return this;\n    }\n\n    /**\n     * @return The scrollbar this container is currently displaying\n     */\n    public Scrollbar scrollbar() {\n        return this.scrollbar;\n    }\n\n    /**\n     * Set the increment, or step size, this container should scroll\n     * by. If this is anything other than {@code 0}, all scrolling in\n     * this container will snap to the closest multiple of this value\n     */\n    public ScrollContainer<C> scrollStep(int scrollStep) {\n        this.scrollStep = scrollStep;\n        return this;\n    }\n\n    /**\n     * @return The current scroll step size of this container\n     */\n    public int scrollStep() {\n        return this.scrollStep;\n    }\n\n    /**\n     * Set a fixed length for the scrollbar of this\n     * container, {@code 0} for dynamic sizing\n     */\n    public ScrollContainer<C> fixedScrollbarLength(int fixedScrollbarLength) {\n        this.fixedScrollbarLength = fixedScrollbarLength;\n        return this;\n    }\n\n    /**\n     * @return The current fixed length of this container's scrollbar,\n     * or {@code 0} if it adjusts based on the content\n     */\n    public int fixedScrollbarLength() {\n        return this.fixedScrollbarLength;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"fixed-scrollbar-length\", UIParsing::parseUnsignedInt, this::fixedScrollbarLength);\n        UIParsing.apply(children, \"scrollbar-thiccness\", UIParsing::parseUnsignedInt, this::scrollbarThiccness);\n        UIParsing.apply(children, \"scrollbar\", Scrollbar::parse, this::scrollbar);\n\n        UIParsing.apply(children, \"scroll-step\", UIParsing::parseUnsignedInt, this::scrollStep);\n    }\n\n    public static ScrollContainer<?> parse(Element element) {\n        return element.getAttribute(\"direction\").equals(\"vertical\")\n                ? UIContainers.verticalScroll(Sizing.content(), Sizing.content(), null)\n                : UIContainers.horizontalScroll(Sizing.content(), Sizing.content(), null);\n    }\n\n    @FunctionalInterface\n    public interface Scrollbar {\n\n        /**\n         * A rectangular scrollbar filled with the given color\n         */\n        static Scrollbar flat(Color color) {\n            int scrollbarColor = color.argb();\n\n            return (context, x, y, width, height, trackX, trackY, trackWidth, trackHeight, lastInteractTime, direction, active) -> {\n                if (!active) return;\n\n                final var progress = Easing.SINE.apply(Mth.clamp(lastInteractTime - System.currentTimeMillis(), 0, 750) / 750f);\n                int alpha = (int) (progress * (scrollbarColor >>> 24));\n\n                context.fill(\n                        x, y, x + width, y + height,\n                        alpha << 24 | (scrollbarColor & 0xFFFFFF)\n                );\n            };\n        }\n\n        /**\n         * The vanilla scrollbar used by the creative inventory screen\n         */\n        static Scrollbar vanilla() {\n            return (context, x, y, width, height, trackX, trackY, trackWidth, trackHeight, lastInteractTime, direction, active) -> {\n                NinePatchTexture.draw(VANILLA_SCROLLBAR_TRACK_TEXTURE, context, trackX, trackY, trackWidth, trackHeight);\n\n                var texture = direction == ScrollDirection.VERTICAL\n                        ? active ? VERTICAL_VANILLA_SCROLLBAR_TEXTURE : DISABLED_VERTICAL_VANILLA_SCROLLBAR_TEXTURE\n                        : active ? HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE : DISABLED_HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE;\n\n                NinePatchTexture.draw(texture, context, x + 1, y + 1, width - 2, height - 2);\n            };\n        }\n\n        /**\n         * The more flat looking vanilla scrollbar used in the\n         * game options screens\n         */\n        static Scrollbar vanillaFlat() {\n            return (context, x, y, width, height, trackX, trackY, trackWidth, trackHeight, lastInteractTime, direction, active) -> {\n                context.fill(trackX, trackY, trackX + trackWidth, trackY + trackHeight, Color.BLACK.argb());\n                NinePatchTexture.draw(FLAT_VANILLA_SCROLLBAR_TEXTURE, context, x, y, width, height);\n            };\n        }\n\n        void draw(OwoUIGraphics context, int x, int y, int width, int height, int trackX, int trackY, int trackWidth, int trackHeight,\n                  long lastInteractTime, ScrollDirection direction, boolean active);\n\n        static Scrollbar parse(Element element) {\n            var children = UIParsing.<Element>allChildrenOfType(element, Node.ELEMENT_NODE);\n            if (children.size() > 1)\n                throw new UIModelParsingException(\"'scrollbar' declaration may only contain a single child\");\n\n            var scrollbarElement = children.get(0);\n            return switch (scrollbarElement.getNodeName()) {\n                case \"vanilla\" -> vanilla();\n                case \"vanilla-flat\" -> vanillaFlat();\n                case \"flat\" -> flat(Color.parse(scrollbarElement));\n                default ->\n                        throw new UIModelParsingException(\"Unknown scrollbar type '\" + scrollbarElement.getNodeName() + \"'\");\n            };\n        }\n    }\n\n    public enum ScrollDirection {\n        VERTICAL(UIComponent::height, UIComponent::updateY, UIComponent::y, Insets::vertical, GLFW.GLFW_KEY_UP, GLFW.GLFW_KEY_DOWN),\n        HORIZONTAL(UIComponent::width, UIComponent::updateX, UIComponent::x, Insets::horizontal, GLFW.GLFW_KEY_LEFT, GLFW.GLFW_KEY_RIGHT);\n\n        public final Function<UIComponent, Integer> sizeGetter;\n        public final BiConsumer<UIComponent, Integer> coordinateSetter;\n        public final Function<ScrollContainer<?>, Integer> coordinateGetter;\n        public final Function<Insets, Integer> insetGetter;\n\n        public final int lessKeycode, moreKeycode;\n\n        ScrollDirection(Function<UIComponent, Integer> sizeGetter, BiConsumer<UIComponent, Integer> coordinateSetter, Function<ScrollContainer<?>, Integer> coordinateGetter, Function<Insets, Integer> insetGetter, int lessKeycode, int moreKeycode) {\n            this.sizeGetter = sizeGetter;\n            this.coordinateSetter = coordinateSetter;\n            this.coordinateGetter = coordinateGetter;\n            this.insetGetter = insetGetter;\n            this.lessKeycode = lessKeycode;\n            this.moreKeycode = moreKeycode;\n        }\n\n        public double choose(double horizontal, double vertical) {\n            return switch (this) {\n                case VERTICAL -> vertical;\n                case HORIZONTAL -> horizontal;\n            };\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/StackLayout.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.base.BaseParentUIComponent;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.Size;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.MountingHelper;\nimport org.apache.commons.lang3.mutable.MutableInt;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.*;\n\npublic class StackLayout extends BaseParentUIComponent {\n\n    protected final List<UIComponent> children = new ArrayList<>();\n    protected final List<UIComponent> childrenView = Collections.unmodifiableList(this.children);\n\n    protected Size contentSize = Size.zero();\n\n    protected StackLayout(Sizing horizontalSizing, Sizing verticalSizing) {\n        super(horizontalSizing, verticalSizing);\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.contentSize.width() + this.padding.get().horizontal();\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return this.contentSize.height() + this.padding.get().vertical();\n    }\n\n    @Override\n    public void layout(Size space) {\n        var childSpace = this.calculateChildSpace(space);\n        this.children.forEach(child -> child.inflate(childSpace));\n\n        var layoutWidth = new MutableInt();\n        var layoutHeight = new MutableInt();\n\n        var layout = new ArrayList<UIComponent>();\n        var helper = MountingHelper.mountEarly(this::mountChild, this.childrenView, child -> {\n            layout.add(child);\n            child.mount(this, this.x + this.padding.get().left() + child.margins().get().left(), this.y + this.padding.get().top() + child.margins().get().top());\n\n            var fullChildSize = child.fullSize();\n            layoutWidth.setValue(Math.max(layoutWidth.getValue(), fullChildSize.width()));\n            layoutHeight.setValue(Math.max(layoutHeight.getValue(), fullChildSize.height()));\n        });\n\n        this.contentSize = Size.of(layoutWidth.intValue(), layoutHeight.intValue());\n        this.applySizing();\n\n        var horizontalAlignment = this.horizontalAlignment();\n        var verticalAlignment = this.verticalAlignment();\n\n        for (var child : layout) {\n            child.updateX(child.baseX() + horizontalAlignment.align(child.fullSize().width(), this.width - this.padding.get().horizontal()));\n            child.updateY(child.baseY() + verticalAlignment.align(child.fullSize().height(), this.height - this.padding.get().vertical()));\n        }\n\n        helper.mountLate();\n    }\n\n    @Override\n    public void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        super.draw(graphics, mouseX, mouseY, partialTicks, delta);\n        this.drawChildren(graphics, mouseX, mouseY, partialTicks, delta, this.children);\n    }\n\n    /**\n     * Add a single child to this layout. If you need to add multiple\n     * children, use {@link #children(Collection)} instead\n     *\n     * @param child The child to append to this layout\n     */\n    public StackLayout child(UIComponent child) {\n        this.children.add(child);\n        this.updateLayout();\n        return this;\n    }\n\n    /**\n     * Add a collection of children to this layout. If you only need to\n     * add a single child to, use {@link #child(UIComponent)} instead\n     *\n     * @param children The children to add to this layout\n     */\n    public StackLayout children(Collection<? extends UIComponent> children) {\n        this.children.addAll(children);\n        this.updateLayout();\n        return this;\n    }\n\n    /**\n     * Insert a single child into this layout. If you need to insert multiple\n     * children, use {@link #children(int, Collection)} instead\n     *\n     * @param index The index at which to insert the child\n     * @param child The child to append to this layout\n     */\n    public StackLayout child(int index, UIComponent child) {\n        this.children.add(index, child);\n        this.updateLayout();\n        return this;\n    }\n\n    /**\n     * Insert a collection of children into this layout. If you only need to\n     * insert a single child to, use {@link #child(int, UIComponent)} instead\n     *\n     * @param index    The index at which to begin inserting children\n     * @param children The children to add to this layout\n     */\n    public StackLayout children(int index, Collection<? extends UIComponent> children) {\n        this.children.addAll(index, children);\n        this.updateLayout();\n        return this;\n    }\n\n    @Override\n    public StackLayout removeChild(UIComponent child) {\n        if (this.children.remove(child)) {\n            child.dismount(DismountReason.REMOVED);\n            this.updateLayout();\n        }\n\n        return this;\n    }\n\n    /**\n     * Remove all children from this layout\n     */\n    public StackLayout clearChildren() {\n        for (var child : this.children) {\n            child.dismount(DismountReason.REMOVED);\n        }\n\n        this.children.clear();\n        this.updateLayout();\n\n        return this;\n    }\n\n    @Override\n    public List<UIComponent> children() {\n        return this.childrenView;\n    }\n\n    @Override\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        final var components = UIParsing\n                .get(children, \"children\", e -> UIParsing.<Element>allChildrenOfType(e, Node.ELEMENT_NODE))\n                .orElse(Collections.emptyList());\n\n        for (var child : components) {\n            this.child(model.parseComponent(UIComponent.class, child));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/UIContainers.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport net.minecraft.network.chat.Component;\n\npublic final class UIContainers {\n\n    private UIContainers() {}\n\n    // ------\n    // Layout\n    // ------\n\n    public static GridLayout grid(Sizing horizontalSizing, Sizing verticalSizing, int rows, int columns) {\n        return new GridLayout(horizontalSizing, verticalSizing, rows, columns);\n    }\n\n    public static FlowLayout verticalFlow(Sizing horizontalSizing, Sizing verticalSizing) {\n        return new FlowLayout(horizontalSizing, verticalSizing, FlowLayout.Algorithm.VERTICAL);\n    }\n\n    public static FlowLayout horizontalFlow(Sizing horizontalSizing, Sizing verticalSizing) {\n        return new FlowLayout(horizontalSizing, verticalSizing, FlowLayout.Algorithm.HORIZONTAL);\n    }\n\n    public static FlowLayout ltrTextFlow(Sizing horizontalSizing, Sizing verticalSizing) {\n        return new FlowLayout(horizontalSizing, verticalSizing, FlowLayout.Algorithm.LTR_TEXT);\n    }\n\n    public static StackLayout stack(Sizing horizontalSizing, Sizing verticalSizing) {\n        return new StackLayout(horizontalSizing, verticalSizing);\n    }\n\n    // ------\n    // Scroll\n    // ------\n\n    public static <C extends UIComponent> ScrollContainer<C> verticalScroll(Sizing horizontalSizing, Sizing verticalSizing, C child) {\n        return new ScrollContainer<>(ScrollContainer.ScrollDirection.VERTICAL, horizontalSizing, verticalSizing, child);\n    }\n\n    public static <C extends UIComponent> ScrollContainer<C> horizontalScroll(Sizing horizontalSizing, Sizing verticalSizing, C child) {\n        return new ScrollContainer<>(ScrollContainer.ScrollDirection.HORIZONTAL, horizontalSizing, verticalSizing, child);\n    }\n\n    // ----------------\n    // Utility wrappers\n    // ----------------\n\n    public static <C extends UIComponent> DraggableContainer<C> draggable(Sizing horizontalSizing, Sizing verticalSizing, C child) {\n        return new DraggableContainer<>(horizontalSizing, verticalSizing, child);\n    }\n\n    public static CollapsibleContainer collapsible(Sizing horizontalSizing, Sizing verticalSizing, Component title, boolean expanded) {\n        return new CollapsibleContainer(horizontalSizing, verticalSizing, title, expanded);\n    }\n\n    public static <C extends UIComponent> OverlayContainer<C> overlay(C child) {\n        return new OverlayContainer<>(child);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/container/WrappingParentUIComponent.java",
    "content": "package io.wispforest.owo.ui.container;\n\nimport io.wispforest.owo.ui.base.BaseParentUIComponent;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Size;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic abstract class WrappingParentUIComponent<C extends UIComponent> extends BaseParentUIComponent {\n\n    protected C child;\n    protected List<UIComponent> childView;\n\n    protected WrappingParentUIComponent(Sizing horizontalSizing, Sizing verticalSizing, C child) {\n        super(horizontalSizing, verticalSizing);\n        this.child = child;\n        this.childView = Collections.singletonList(this.child);\n    }\n\n    @Override\n    protected int determineHorizontalContentSize(Sizing sizing) {\n        return this.child.fullSize().width() + this.padding.get().horizontal();\n    }\n\n    @Override\n    protected int determineVerticalContentSize(Sizing sizing) {\n        return this.child.fullSize().height() + this.padding.get().vertical();\n    }\n\n    @Override\n    public void layout(Size space) {\n        this.child.inflate(this.calculateChildSpace(space));\n        this.child.mount(this, this.childMountX(), this.childMountY());\n    }\n\n    /**\n     * @return The x-coordinate at which to mount the child\n     */\n    protected int childMountX() {\n        return this.x + child.margins().get().left() + this.padding.get().left();\n    }\n\n    /**\n     * @return The y-coordinate at which to mount the child\n     */\n    protected int childMountY() {\n        return this.y + child.margins().get().top() + this.padding.get().top();\n    }\n\n    public WrappingParentUIComponent<C> child(C newChild) {\n        if (this.child != null) {\n            this.child.dismount(DismountReason.REMOVED);\n        }\n\n        this.child = newChild;\n        this.childView = Collections.singletonList(this.child);\n\n        this.updateLayout();\n        return this;\n    }\n\n    public C child() {\n        return this.child;\n    }\n\n    @Override\n    public List<UIComponent> children() {\n        return this.childView;\n    }\n\n    @Override\n    public ParentUIComponent removeChild(UIComponent child) {\n        throw new UnsupportedOperationException(\"Cannot remove the child of a wrapping component\");\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        super.parseProperties(model, element, children);\n\n        try {\n            var childList = UIParsing.<Element>allChildrenOfType(element, Node.ELEMENT_NODE);\n            this.child((C) model.parseComponent(UIComponent.class, childList.get(0)));\n        } catch (UIModelParsingException exception) {\n            throw new UIModelParsingException(\"Could not initialize container child\", exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Animatable.java",
    "content": "package io.wispforest.owo.ui.core;\n\npublic interface Animatable<T extends Animatable<T>> {\n\n    T interpolate(T next, float delta);\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/AnimatableProperty.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.util.Observable;\nimport org.jetbrains.annotations.Nullable;\n\n/**\n * A container which holds an animatable object,\n * used to manage to properties of UI components. Extends\n * the {@link Observable} container so that changes in its value\n * can be propagated to the holder of the property\n *\n * @param <A> The type of animatable object this property describes\n */\npublic class AnimatableProperty<A extends Animatable<A>> extends Observable<A> {\n\n    protected @Nullable Animation<A> animation;\n\n    protected AnimatableProperty(A initial) {\n        super(initial);\n    }\n\n    /**\n     * Creates a new animatable property with\n     * the given initial value\n     */\n    public static <A extends Animatable<A>> AnimatableProperty<A> of(A initial) {\n        return new AnimatableProperty<>(initial);\n    }\n\n    /**\n     * Create an animation object which interpolates the state of this\n     * property from the current one to {@code to} in {@code duration}\n     * milliseconds, applying the given easing\n     * <p>\n     * This method replaces the current animation object of\n     * this property - it will not be updated anymore\n     *\n     * @param duration The duration of the animation to create, in milliseconds\n     * @param easing   The easing method to use\n     * @param to       The target state of this property\n     * @return The new animation of this property.\n     */\n    public Animation<A> animate(int duration, Easing easing, A to) {\n        this.animation = new Animation<>(duration, this::set, easing, this.value, to);\n        return this.animation;\n    }\n\n    /**\n     * @return The current animation object of this property,\n     * potentially {@code null} if {@link #animate(int, Easing, Animatable)}\n     * was never called\n     */\n    public @Nullable Animation<A> animation() {\n        return this.animation;\n    }\n\n    /**\n     * Update the currently stored animation\n     * object of this property\n     *\n     * @param delta The duration of the last frame, in partial ticks\n     */\n    public void update(float delta) {\n        if (this.animation == null) return;\n        this.animation.update(delta);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Animation.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.util.Mth;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.function.Consumer;\n\npublic class Animation<A extends Animatable<A>> {\n\n    private final int duration;\n\n    private float delta = 0;\n    private Direction direction = Direction.BACKWARDS;\n    private boolean looping = false;\n\n    private final Consumer<A> setter;\n    private final Easing easing;\n\n    private final A from;\n    private final A to;\n\n    private final EventStream<Finished> finishedEvents = Finished.newStream();\n    private boolean eventInvoked = true;\n\n    public Animation(int duration, Consumer<A> setter, Easing easing, A from, A to) {\n        this.duration = duration;\n        this.setter = setter;\n        this.easing = easing;\n        this.from = from;\n        this.to = to;\n    }\n\n    public static Composed compose(Animation<?>... elements) {\n        return new Composed(elements);\n    }\n\n    public void update(float delta) {\n        if (this.delta == this.direction.targetDelta) {\n            if (!this.eventInvoked) {\n                this.finishedEvents.sink().onFinished(this.direction, this.looping);\n                this.eventInvoked = true;\n            }\n\n            if (this.looping) this.reverse();\n            else return;\n        }\n\n        this.delta = Mth.clamp(this.delta + (delta * 50 / duration) * this.direction.multiplier, 0, 1);\n\n        this.setter.accept(this.from.interpolate(this.to, this.easing.apply(this.delta)));\n    }\n\n    public Animation<A> forwards() {\n        this.setDirection(Direction.FORWARDS);\n        return this;\n    }\n\n    public Animation<A> backwards() {\n        this.setDirection(Direction.BACKWARDS);\n        return this;\n    }\n\n    public Animation<A> reverse() {\n        this.setDirection(this.direction.reversed());\n        return this;\n    }\n\n    private void setDirection(Direction direction) {\n        if (this.direction == direction) return;\n        this.direction = direction;\n        this.eventInvoked = false;\n    }\n\n    public Animation<A> loop(boolean loop) {\n        this.looping = loop;\n        return this;\n    }\n\n    public boolean looping() {\n        return this.looping;\n    }\n\n    public Direction direction() {\n        return this.direction;\n    }\n\n    public EventSource<Finished> finished() {\n        return this.finishedEvents.source();\n    }\n\n    public enum Direction {\n        FORWARDS(1, 1),\n        BACKWARDS(-1, 0);\n\n        public final int multiplier;\n        public final float targetDelta;\n\n        Direction(int multiplier, float targetDelta) {\n            this.multiplier = multiplier;\n            this.targetDelta = targetDelta;\n        }\n\n        public Direction reversed() {\n            return switch (this) {\n                case FORWARDS -> BACKWARDS;\n                case BACKWARDS -> FORWARDS;\n            };\n        }\n    }\n\n    public interface Finished {\n        void onFinished(Direction direction, boolean looping);\n\n        static EventStream<Finished> newStream() {\n            return new EventStream<>(subscribers -> (direction, looping) -> {\n                for (var subscriber : subscribers) {\n                    subscriber.onFinished(direction, looping);\n                }\n            });\n        }\n    }\n\n    public static class Composed {\n        private final List<Animation<?>> elements;\n\n        private Composed(Animation<?>... elements) {\n            this.elements = Arrays.asList(elements);\n        }\n\n        public void forwards() {\n            this.elements.forEach(Animation::forwards);\n        }\n\n        public void backwards() {\n            this.elements.forEach(Animation::backwards);\n        }\n\n        public void reverse() {\n            this.elements.forEach(Animation::reverse);\n        }\n\n        public void loop(boolean loop) {\n            this.elements.forEach(animation -> animation.loop(loop));\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Color.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport com.google.common.collect.ImmutableMap;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.util.Mth;\nimport net.minecraft.world.item.DyeColor;\nimport org.jetbrains.annotations.NotNull;\nimport org.w3c.dom.Node;\n\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.stream.Stream;\n\npublic record Color(float red, float green, float blue, float alpha) implements Animatable<Color> {\n\n    public static final Endec<Color> RGBA_HEX_ENDEC = Endec.STRING.xmap(\n            string -> Color.ofArgb(Integer.parseUnsignedInt(string.substring(1), 16)),\n            color -> color.asHexString(true)\n    );\n\n    public static final Color BLACK = Color.ofRgb(0);\n    public static final Color WHITE = Color.ofRgb(0xFFFFFF);\n    public static final Color RED = Color.ofRgb(0xFF0000);\n    public static final Color GREEN = Color.ofRgb(0x00FF00);\n    public static final Color BLUE = Color.ofRgb(0x0000FF);\n\n    private static final Map<String, Color> NAMED_TEXT_COLORS = Stream.of(ChatFormatting.values())\n            .filter(ChatFormatting::isColor)\n            .collect(ImmutableMap.toImmutableMap(formatting -> {\n                return formatting.getName().toLowerCase(Locale.ROOT).replace(\"_\", \"-\");\n            }, Color::ofFormatting));\n\n    public Color(float red, float green, float blue) {\n        this(red, green, blue, 1f);\n    }\n\n    public static Color ofArgb(int argb) {\n        return new Color(\n                ((argb >> 16) & 0xFF) / 255f,\n                ((argb >> 8) & 0xFF) / 255f,\n                (argb & 0xFF) / 255f,\n                (argb >>> 24) / 255f\n        );\n    }\n\n    public static Color ofRgb(int rgb) {\n        return new Color(\n                ((rgb >> 16) & 0xFF) / 255f,\n                ((rgb >> 8) & 0xFF) / 255f,\n                (rgb & 0xFF) / 255f,\n                1f\n        );\n    }\n\n    public static Color ofHsv(float hue, float saturation, float value) {\n        // we call .5e-7f the magic \"do not turn a hue value of 1f into yellow\" constant\n        return ofRgb(Mth.hsvToRgb(hue - .5e-7f, saturation, value));\n    }\n\n    public static Color ofHsv(float hue, float saturation, float value, float alpha) {\n        // we call .5e-7f the magic \"do not turn a hue value of 1f into yellow\" constant\n        return ofArgb((int) (alpha * 255) << 24 | Mth.hsvToRgb(hue - .5e-7f, saturation, value));\n    }\n\n    public static Color ofFormatting(@NotNull ChatFormatting formatting) {\n        var colorValue = formatting.getColor();\n        return ofRgb(colorValue == null ? 0 : colorValue);\n    }\n\n    public static Color ofDye(@NotNull DyeColor dyeColor) {\n        return ofArgb(dyeColor.getTextureDiffuseColor());\n    }\n\n    /**\n     * Generates a random color\n     * @apiNote Don't tell glisco about this\n     * @author chyzman\n     */\n    public static Color random() {\n        return ofArgb((int) (Math.random() * 0xFFFFFF) | 0xFF000000);\n    }\n\n    public int rgb() {\n        return (int) (this.red * 255) << 16\n                | (int) (this.green * 255) << 8\n                | (int) (this.blue * 255);\n    }\n\n    public int argb() {\n        return (int) (this.alpha * 255) << 24\n                | (int) (this.red * 255) << 16\n                | (int) (this.green * 255) << 8\n                | (int) (this.blue * 255);\n    }\n\n    public float[] hsv() {\n        float hue, saturation, value;\n\n        float cmax = Math.max(Math.max(this.red, this.green), this.blue);\n        float cmin = Math.min(Math.min(this.red, this.green), this.blue);\n\n        value = cmax;\n        if (cmax != 0) {\n            saturation = (cmax - cmin) / cmax;\n        } else {\n            saturation = 0;\n        }\n\n        if (saturation == 0) {\n            hue = 0;\n        } else {\n            float redc = (cmax - this.red) / (cmax - cmin);\n            float greenc = (cmax - this.green) / (cmax - cmin);\n            float bluec = (cmax - this.blue) / (cmax - cmin);\n\n            if (this.red == cmax) {\n                hue = bluec - greenc;\n            } else if (this.green == cmax)\n                hue = 2.0f + redc - bluec;\n            else {\n                hue = 4.0f + greenc - redc;\n            }\n\n            hue = hue / 6.0f;\n            if (hue < 0) hue = hue + 1.0f;\n        }\n\n        return new float[]{hue, saturation, value, this.alpha};\n    }\n\n    public String asHexString(boolean includeAlpha) {\n        return includeAlpha\n                ? String.format(\"#%08X\", this.argb())\n                : String.format(\"#%06X\", this.rgb());\n    }\n\n    public io.wispforest.owo.braid.core.Color toBraid() {\n        return io.wispforest.owo.braid.core.Color.values(this.red, this.green, this.blue, this.alpha);\n    }\n\n    @Override\n    public Color interpolate(Color next, float delta) {\n        return new Color(\n                Mth.lerp(delta, this.red, next.red),\n                Mth.lerp(delta, this.green, next.green),\n                Mth.lerp(delta, this.blue, next.blue),\n                Mth.lerp(delta, this.alpha, next.alpha)\n        );\n    }\n\n    /**\n     * Tries to interpret the given node's text content as a color\n     * in {@code #RRGGBB} or {@code #AARRGGBB} format, or as\n     * the name of a text color\n     *\n     * @return The parsed color as an unsigned integer\n     * @throws UIModelParsingException If the text content does not match\n     *                                 the expected color format\n     */\n    public static Color parse(Node node) {\n        var text = node.getTextContent().strip();\n\n        if (!text.startsWith(\"#\")) {\n            var color = NAMED_TEXT_COLORS.get(text);\n            if (color != null) {\n                return color;\n            } else {\n                throw new UIModelParsingException(\"Invalid color value '\" + text + \"', expected hex color of format #RRGGBB or #AARRGGBB or named text color\");\n            }\n        } else {\n            if (text.matches(\"#([A-Fa-f\\\\d]{2}){3,4}\")) {\n                return text.length() == 7\n                        ? Color.ofRgb(Integer.parseUnsignedInt(text.substring(1), 16))\n                        : Color.ofArgb(Integer.parseUnsignedInt(text.substring(1), 16));\n            } else {\n                throw new UIModelParsingException(\"Invalid color value '\" + text + \"', expected hex color of format #RRGGBB or #AARRGGBB or named text color\");\n            }\n        }\n    }\n\n    public static int parseAndPack(Node node) {\n        return parse(node).argb();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/CursorStyle.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport org.lwjgl.glfw.GLFW;\n\npublic enum CursorStyle {\n    /**\n     * The default cursor style defined by\n     * the operating system\n     */\n    NONE(0),\n    /**\n     * The default arrow-style pointing cursor\n     */\n    POINTER(GLFW.GLFW_ARROW_CURSOR),\n\n    /**\n     * The text selection, usually I-beam, cursor\n     */\n    TEXT(GLFW.GLFW_IBEAM_CURSOR),\n\n    /**\n     * The hand cursor which signals clickable areas\n     */\n    HAND(GLFW.GLFW_HAND_CURSOR),\n\n    /**\n     * the Crosshair cursor\n     */\n    CROSSHAIR(GLFW.GLFW_CROSSHAIR_CURSOR),\n\n    /**\n     * The cross-shaped cursor which signals\n     * draggable/movable areas\n     */\n    MOVE(GLFW.GLFW_RESIZE_ALL_CURSOR),\n\n    /**\n     * The horizontal resize cursor\n     * @see #VERTICAL_RESIZE\n     */\n    HORIZONTAL_RESIZE(GLFW.GLFW_HRESIZE_CURSOR),\n\n    /**\n     * The vertical resize cursor\n     * @see #HORIZONTAL_RESIZE\n     */\n    VERTICAL_RESIZE(GLFW.GLFW_VRESIZE_CURSOR),\n\n    /**\n     * The NorthWest-SouthEast resize cursor\n     * @see #NESW_RESIZE\n     *\n     * @implNote This cursor style is not necessarily supported by all cursor themes\n     */\n    NWSE_RESIZE(GLFW.GLFW_RESIZE_NWSE_CURSOR),\n\n    /**\n     * The NorthEast-SouthWest resize cursor\n     * @see #NWSE_RESIZE\n     *\n     * @implNote This cursor style is not necessarily supported by all cursor themes\n     */\n    NESW_RESIZE(GLFW.GLFW_RESIZE_NESW_CURSOR),\n\n\n    /**\n     * The Not-Allowed cursor style\n     *\n     * @implNote This cursor style is not necessarily supported by all cursor themes\n     */\n    NOT_ALLOWED(GLFW.GLFW_NOT_ALLOWED_CURSOR);\n\n\n    public final int glfw;\n\n    CursorStyle(int glfw) {this.glfw = glfw;}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Easing.java",
    "content": "package io.wispforest.owo.ui.core;\n\n/**\n * An easing function which can smoothly move\n * an interpolation value from 0 to 1\n */\npublic interface Easing {\n\n    Easing LINEAR = x -> x;\n\n    Easing SINE = x -> {\n        return (float) (Math.sin(x * Math.PI - Math.PI / 2) * 0.5 + 0.5);\n    };\n\n    Easing QUADRATIC = x -> {\n        return x < 0.5 ? 2 * x * x : (float) (1 - Math.pow(-2 * x + 2, 2) / 2);\n    };\n\n    Easing CUBIC = x -> {\n        return x < 0.5 ? 4 * x * x * x : (float) (1 - Math.pow(-2 * x + 2, 3) / 2);\n    };\n\n    Easing QUARTIC = x -> {\n        return x < 0.5 ? 8 * x * x * x * x : (float) (1 - Math.pow(-2 * x + 2, 4) / 2);\n    };\n\n    Easing EXPO = x -> {\n        if (x == 0) return 0;\n        if (x == 1) return 1;\n\n        return x < 0.5\n                ? (float) Math.pow(2, 20 * x - 10) / 2\n                : (2 - (float) Math.pow(2, -20 * x + 10)) / 2;\n    };\n\n    float apply(float x);\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/HorizontalAlignment.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport org.w3c.dom.Element;\n\nimport java.util.Locale;\n\npublic enum HorizontalAlignment {\n    LEFT, CENTER, RIGHT;\n\n    public int align(int componentWidth, int span) {\n        return switch (this) {\n            case LEFT -> 0;\n            case CENTER -> span / 2 - componentWidth / 2;\n            case RIGHT -> span - componentWidth;\n        };\n    }\n\n    public static HorizontalAlignment parse(Element element) {\n        return valueOf(element.getTextContent().strip().toUpperCase(Locale.ROOT));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Insets.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport net.minecraft.util.Mth;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\npublic record Insets(int top, int bottom, int left, int right) implements Animatable<Insets> {\n\n    private static final Insets NONE = new Insets(0, 0, 0, 0);\n\n    @ApiStatus.Internal\n    @Deprecated(forRemoval = true)\n    public Insets {}\n\n    public Insets inverted() {\n        return new Insets(-this.top, -this.bottom, -this.left, -this.right);\n    }\n\n    public Insets add(int top, int bottom, int left, int right) {\n        return new Insets(this.top + top, this.bottom + bottom, this.left + left, this.right + right);\n    }\n\n    public Insets withTop(int top) {\n        return new Insets(top, this.bottom, this.left, this.right);\n    }\n\n    public Insets withBottom(int bottom) {\n        return new Insets(this.top, bottom, this.left, this.right);\n    }\n\n    public Insets withLeft(int left) {\n        return new Insets(this.top, this.bottom, left, this.right);\n    }\n\n    public Insets withRight(int right) {\n        return new Insets(this.top, this.bottom, this.left, right);\n    }\n\n    public int horizontal() {\n        return this.left + this.right;\n    }\n\n    public int vertical() {\n        return this.top + this.bottom;\n    }\n\n    @Override\n    public Insets interpolate(Insets next, float delta) {\n        return new Insets(\n                (int) Mth.lerpInt(delta, this.top, next.top),\n                (int) Mth.lerpInt(delta, this.bottom, next.bottom),\n                (int) Mth.lerpInt(delta, this.left, next.left),\n                (int) Mth.lerpInt(delta, this.right, next.right)\n        );\n    }\n\n    public static Insets both(int horizontal, int vertical) {\n        return new Insets(vertical, vertical, horizontal, horizontal);\n    }\n\n    public static Insets top(int top) {\n        return new Insets(top, 0, 0, 0);\n    }\n\n    public static Insets bottom(int bottom) {\n        return new Insets(0, bottom, 0, 0);\n    }\n\n    public static Insets left(int left) {\n        return new Insets(0, 0, left, 0);\n    }\n\n    public static Insets right(int right) {\n        return new Insets(0, 0, 0, right);\n    }\n\n    public static Insets of(int top, int bottom, int left, int right) {\n        return new Insets(top, bottom, left, right);\n    }\n\n    public static Insets of(int inset) {\n        return new Insets(inset, inset, inset, inset);\n    }\n\n    public static Insets vertical(int inset) {\n        return new Insets(inset, inset, 0, 0);\n    }\n\n    public static Insets horizontal(int inset) {\n        return new Insets(0, 0, inset, inset);\n    }\n\n    public static Insets none() {\n        return NONE;\n    }\n\n    public static Insets parse(Element insetsElement) {\n        int top = 0, bottom = 0, left = 0, right = 0;\n\n        for (var node : UIParsing.<Element>allChildrenOfType(insetsElement, Node.ELEMENT_NODE)) {\n            try {\n                int value = Integer.parseInt(node.getTextContent().strip());\n\n                switch (node.getNodeName()) {\n                    case \"top\" -> top = value;\n                    case \"bottom\" -> bottom = value;\n                    case \"left\" -> left = value;\n                    case \"right\" -> right = value;\n                    case \"all\" -> right = left = top = bottom = value;\n                    case \"vertical\" -> top = bottom = value;\n                    case \"horizontal\" -> left = right = value;\n                }\n            } catch (NumberFormatException exception) {\n                throw new UIModelParsingException(\"Non-int value in inset declaration\");\n            }\n        }\n\n        return of(top, bottom, left, right);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/OwoUIAdapter.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.renderdoc.RenderDoc;\nimport io.wispforest.owo.ui.util.CursorAdapter;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.Renderable;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.gui.narration.NarratableEntry;\nimport net.minecraft.client.gui.narration.NarrationElementOutput;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.function.BiFunction;\n\n/**\n * A UI adapter constitutes the main entrypoint to using owo-ui.\n * It takes care of rendering the UI tree correctly, handles input events\n * and cursor styling as well as the component inspector.\n * <p>\n * Additionally, the adapter implements all interfaces required for it\n * to be treated as a normal widget by the vanilla screen system - this means\n * even if you choose to not use {@link io.wispforest.owo.ui.base.BaseOwoScreen}\n * you can always simply add it as a widget and get most of the functionality\n * working out of the box\n * <p>\n * To draw the UI tree managed by this adapter, call {@link OwoUIAdapter#render(GuiGraphics, int, int, float)}.\n * Note that this does not draw the current tooltip of the UI - this must be done separately\n * by invoking {@link #drawTooltip(GuiGraphics, int, int, float)}. If in a scenario with multiple adapters\n * or other sources rendering UI elements to the screen, it is generally desirable to delay tooltip\n * drawing until after all UI is drawn to avoid layering issues.\n *\n * @see io.wispforest.owo.ui.base.BaseOwoScreen\n */\npublic class OwoUIAdapter<R extends ParentUIComponent> implements GuiEventListener, Renderable, NarratableEntry {\n\n    private static boolean isRendering = false;\n\n    public final R rootComponent;\n    public final CursorAdapter cursorAdapter;\n\n    protected boolean disposed = false;\n    protected boolean captureFrame = false;\n\n    protected int x, y;\n    protected int width, height;\n\n    public boolean enableInspector = false;\n    public boolean globalInspector = false;\n    public int inspectorZOffset = 1000;\n\n    protected OwoUIAdapter(int x, int y, int width, int height, R rootComponent) {\n        this.x = x;\n        this.y = y;\n        this.width = width;\n        this.height = height;\n\n        this.cursorAdapter = CursorAdapter.ofClientWindow();\n        this.rootComponent = rootComponent;\n    }\n\n    /**\n     * Create a UI adapter for the given screen. This also sets it up\n     * to be rendered and receive input events, without needing you to\n     * do any more setup\n     *\n     * @param screen             The screen for which to create an adapter\n     * @param rootComponentMaker A function which will create the root component of this screen\n     * @param <R>                The type of root component the created adapter will use\n     * @return The new UI adapter, already set up for the given screen\n     */\n    public static <R extends ParentUIComponent> OwoUIAdapter<R> create(Screen screen, BiFunction<Sizing, Sizing, R> rootComponentMaker) {\n        var rootComponent = rootComponentMaker.apply(Sizing.fill(100), Sizing.fill(100));\n\n        var adapter = new OwoUIAdapter<>(0, 0, screen.width, screen.height, rootComponent);\n        screen.addRenderableWidget(adapter);\n        screen.setFocused(adapter);\n\n        return adapter;\n    }\n\n    /**\n     * Create a new UI adapter without the specific context of a screen - use this\n     * method when you want to embed owo-ui into a different context\n     *\n     * @param x                  The x-coordinate of the top-left corner of the root component\n     * @param y                  The y-coordinate of the top-left corner of the root component\n     * @param width              The width of the available area, in pixels\n     * @param height             The height of the available area, in pixels\n     * @param rootComponentMaker A function which will create the root component of the adapter\n     * @param <R>                The type of root component the created adapter will use\n     * @return The new UI adapter, ready for layout inflation\n     */\n    public static <R extends ParentUIComponent> OwoUIAdapter<R> createWithoutScreen(int x, int y, int width, int height, BiFunction<Sizing, Sizing, R> rootComponentMaker) {\n        var rootComponent = rootComponentMaker.apply(Sizing.fill(100), Sizing.fill(100));\n        return new OwoUIAdapter<>(x, y, width, height, rootComponent);\n    }\n\n    /**\n     * Begin the layout process of the UI tree and\n     * mount the tree once the layout is inflated\n     * <p>\n     * After this method has executed, this adapter is ready for rendering\n     */\n    public void inflateAndMount() {\n        this.rootComponent.inflate(Size.of(this.width, this.height));\n        this.rootComponent.mount(null, this.x, this.y);\n    }\n\n    public void moveAndResize(int x, int y, int width, int height) {\n        this.x = x;\n        this.y = y;\n        this.width = width;\n        this.height = height;\n\n        this.inflateAndMount();\n    }\n\n    /**\n     * Dispose this UI adapter - this will destroy the cursor\n     * objects held onto by this adapter and stop updating the cursor style\n     * <p>\n     * After this method has executed, this adapter can safely be garbage-collected\n     */\n    // TODO properly dispose root component\n    public void dispose() {\n        this.cursorAdapter.dispose();\n        this.disposed = true;\n    }\n\n    /**\n     * @return Toggle rendering of the inspector\n     */\n    public boolean toggleInspector() {\n        return this.enableInspector = !this.enableInspector;\n    }\n\n    /**\n     * @return Toggle the inspector between\n     * hovered and global mode\n     */\n    public boolean toggleGlobalInspector() {\n        return this.globalInspector = !this.globalInspector;\n    }\n\n    public int x() {\n        return this.x;\n    }\n\n    public int y() {\n        return this.y;\n    }\n\n    public int width() {\n        return this.width;\n    }\n\n    public int height() {\n        return this.height;\n    }\n\n    @Override\n    public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {\n        if (!(graphics instanceof OwoUIGraphics)) graphics = OwoUIGraphics.of(graphics);\n        var owoGraphics = (OwoUIGraphics) graphics;\n\n        try {\n            isRendering = true;\n\n            if (this.captureFrame) RenderDoc.startFrameCapture();\n\n            final var delta = Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaTicks();\n            final var window = Minecraft.getInstance().getWindow();\n\n            this.rootComponent.update(delta, mouseX, mouseY);\n\n            graphics.enableScissor(0, 0, window.getWidth(), window.getHeight());\n            this.rootComponent.draw(owoGraphics, mouseX, mouseY, partialTicks, delta);\n            graphics.disableScissor();\n\n            final var hovered = this.rootComponent.childAt(mouseX, mouseY);\n            if (!disposed && hovered != null) {\n                this.cursorAdapter.applyStyle(hovered.cursorStyle());\n            }\n\n            if (this.enableInspector) {\n                OwoUIGraphics.drawInspector(owoGraphics, this.rootComponent, mouseX, mouseY, !this.globalInspector);\n            }\n\n            if (this.captureFrame) RenderDoc.endFrameCapture();\n        } finally {\n            isRendering = false;\n            this.captureFrame = false;\n        }\n    }\n\n    /**\n     * Draw the current tooltip of the UI managed by this adapter. This method\n     * must not be called without a previous, corresponding call to {@link #render(GuiGraphics, int, int, float)}\n     *\n     * @since 0.12.19\n     */\n    public void drawTooltip(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {\n        if (!(graphics instanceof OwoUIGraphics)) graphics = OwoUIGraphics.of(graphics);\n        var owoContext = (OwoUIGraphics) graphics;\n\n        final var delta = Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaTicks();\n\n        this.rootComponent.drawTooltip(owoContext, mouseX, mouseY, partialTicks, delta);\n        graphics.renderDeferredElements();\n    }\n\n    @Override\n    public boolean isMouseOver(double mouseX, double mouseY) {\n        return this.rootComponent.isInBoundingBox(mouseX, mouseY);\n    }\n\n    @Override\n    public void setFocused(boolean focused) {}\n\n    @Override\n    public boolean isFocused() {\n        return true;\n    }\n\n    @Override\n    public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {\n        return this.rootComponent.onMouseDown(click, doubled);\n    }\n\n    @Override\n    public boolean mouseReleased(MouseButtonEvent click) {\n        return this.rootComponent.onMouseUp(click);\n    }\n\n    @Override\n    public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {\n        return this.rootComponent.onMouseScroll(mouseX, mouseY, verticalAmount);\n    }\n\n    @Override\n    public boolean mouseDragged(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.rootComponent.onMouseDrag(click, deltaX, deltaY);\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (Owo.DEBUG && input.key() == GLFW.GLFW_KEY_LEFT_SHIFT) {\n            if (input.hasControlDown()) {\n                this.toggleInspector();\n            } else if (input.hasAltDown()) {\n                this.toggleGlobalInspector();\n            }\n        }\n\n        if (Owo.DEBUG && input.key() == GLFW.GLFW_KEY_R && RenderDoc.isAvailable()) {\n            if (input.hasAltDown() && input.hasControlDown()) {\n                this.captureFrame = true;\n            }\n        }\n\n        return this.rootComponent.onKeyPress(input);\n    }\n\n    @Override\n    public boolean charTyped(CharacterEvent input) {\n        return this.rootComponent.onCharTyped(input);\n    }\n\n    @Override\n    public NarrationPriority narrationPriority() {\n        return NarrationPriority.NONE;\n    }\n\n    @Override\n    public void updateNarration(NarrationElementOutput builder) {}\n\n    public static boolean isRendering() {\n        return isRendering;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/OwoUIGraphics.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport com.google.common.base.Preconditions;\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport io.wispforest.owo.mixin.ui.access.GuiGraphicsAccessor;\nimport io.wispforest.owo.ui.event.WindowResizeCallback;\nimport io.wispforest.owo.ui.renderstate.CircleElementRenderState;\nimport io.wispforest.owo.ui.renderstate.GradientQuadElementRenderState;\nimport io.wispforest.owo.ui.renderstate.LineElementRenderState;\nimport io.wispforest.owo.ui.renderstate.RingElementRenderState;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.navigation.ScreenPosition;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.state.GuiRenderState;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner;\nimport net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\npublic class OwoUIGraphics extends GuiGraphics {\n\n    public static final Identifier PANEL_NINE_PATCH_TEXTURE = Identifier.fromNamespaceAndPath(\"owo\", \"panel/default\");\n    public static final Identifier DARK_PANEL_NINE_PATCH_TEXTURE = Identifier.fromNamespaceAndPath(\"owo\", \"panel/dark\");\n    public static final Identifier PANEL_INSET_NINE_PATCH_TEXTURE = Identifier.fromNamespaceAndPath(\"owo\", \"panel/inset\");\n\n    private final Consumer<Runnable> setTooltipDrawer;\n\n    protected OwoUIGraphics(Minecraft client, GuiRenderState renderState, int mouseX, int mouseY, Consumer<Runnable> setTooltipDrawer) {\n        super(client, renderState, mouseX, mouseY);\n        this.setTooltipDrawer = setTooltipDrawer;\n    }\n\n    public static OwoUIGraphics of(GuiGraphics graphics) {\n        var owoContext = new OwoUIGraphics(\n            Minecraft.getInstance(),\n            graphics.guiRenderState,\n            ((GuiGraphicsAccessor) graphics).owo$getMouseY(),\n            ((GuiGraphicsAccessor) graphics).owo$getMouseX(),\n            ((GuiGraphicsAccessor) graphics)::owo$setDeferredTooltip\n        );\n\n        ((GuiGraphicsAccessor) owoContext).owo$setScissorStack(((GuiGraphicsAccessor) graphics).owo$getScissorStack());\n        ((GuiGraphicsAccessor) owoContext).owo$setPose(((GuiGraphicsAccessor) graphics).owo$getPose());\n\n        return owoContext;\n    }\n\n    public static UtilityScreen utilityScreen() {\n        return UtilityScreen.get();\n    }\n\n    public boolean intersectsScissor(PositionedRectangle other) {\n        other = other.transform(getMatrixStack());\n\n        var rect = this.scissorStack.peek();\n\n        if (rect == null) return true;\n\n        var pos = rect.position();\n\n        return other.x() < pos.x() + rect.width()\n            && other.x() + other.width() >= pos.x()\n            && other.y() < pos.y() + rect.height()\n            && other.y() + other.height() >= pos.y();\n    }\n\n    public void drawRectOutline(int x, int y, int width, int height, int color) {\n        drawRectOutline(RenderPipelines.GUI, x, y, width, height, color);\n    }\n\n    /**\n     * Draw the outline of a rectangle\n     *\n     * @param x      The x-coordinate of top-left corner of the rectangle\n     * @param y      The y-coordinate of top-left corner of the rectangle\n     * @param width  The width of the rectangle\n     * @param height The height of the rectangle\n     * @param color  The color of the rectangle\n     */\n    public void drawRectOutline(RenderPipeline pipeline, int x, int y, int width, int height, int color) {\n        this.fill(pipeline, x, y, x + width, y + 1, color);\n        this.fill(pipeline, x, y + height - 1, x + width, y + height, color);\n\n        this.fill(pipeline, x, y + 1, x + 1, y + height - 1, color);\n        this.fill(pipeline, x + width - 1, y + 1, x + width, y + height - 1, color);\n    }\n\n    public void drawGradientRect(int x, int y, int width, int height, int topLeftColor, int topRightColor, int bottomRightColor, int bottomLeftColor) {\n        this.drawGradientRect(RenderPipelines.GUI, x, y, width, height, topLeftColor, topRightColor, bottomRightColor, bottomLeftColor);\n    }\n\n    /**\n     * Draw a filled rectangle with a gradient\n     *\n     * @param x                The x-coordinate of top-left corner of the rectangle\n     * @param y                The y-coordinate of top-left corner of the rectangle\n     * @param width            The width of the rectangle\n     * @param height           The height of the rectangle\n     * @param topLeftColor     The color at the rectangle's top left corner\n     * @param topRightColor    The color at the rectangle's top right corner\n     * @param bottomRightColor The color at the rectangle's bottom right corner\n     * @param bottomLeftColor  The color at the rectangle's bottom left corner\n     */\n    public void drawGradientRect(RenderPipeline pipeline, int x, int y, int width, int height, int topLeftColor, int topRightColor, int bottomRightColor, int bottomLeftColor) {\n        this.guiRenderState.submitGuiElement(new GradientQuadElementRenderState(\n            pipeline,\n            new Matrix3x2f(this.pose()),\n            new ScreenRectangle(new ScreenPosition(x, y), width, height),\n            this.scissorStack.peek(),\n            Color.ofArgb(topLeftColor),\n            Color.ofArgb(topRightColor),\n            Color.ofArgb(bottomLeftColor),\n            Color.ofArgb(bottomRightColor)\n        ));\n    }\n\n    /**\n     * Draw a panel that looks like the background of a vanilla\n     * inventory screen\n     *\n     * @param x      The x-coordinate of top-left corner of the panel\n     * @param y      The y-coordinate of top-left corner of the panel\n     * @param width  The width of the panel\n     * @param height The height of the panel\n     * @param dark   Whether to use the dark version of the panel texture\n     */\n    public void drawPanel(int x, int y, int width, int height, boolean dark) {\n        NinePatchTexture.draw(dark ? DARK_PANEL_NINE_PATCH_TEXTURE : PANEL_NINE_PATCH_TEXTURE, this, x, y, width, height);\n    }\n\n    public void drawSpectrum(int x, int y, int width, int height, boolean vertical) {\n        this.guiRenderState.submitGuiElement(new GradientQuadElementRenderState(\n            OwoUIPipelines.GUI_HSV,\n            new Matrix3x2f(this.pose()),\n            new ScreenRectangle(new ScreenPosition(x, y), width, height),\n            this.scissorStack.peek(),\n            Color.WHITE,\n            new Color(vertical ? 1f : 0f, 1f, 1f),\n            new Color(vertical ? 0f : 1f, 1f, 1f),\n            new Color(0f, 1f, 1f)\n        ));\n    }\n\n    public void drawText(Component text, float x, float y, float scale, int color) {\n        drawText(text, x, y, scale, color, TextAnchor.TOP_LEFT);\n    }\n\n    public void drawText(Component text, float x, float y, float scale, int color, TextAnchor anchorPoint) {\n        final var textRenderer = Minecraft.getInstance().font;\n\n        this.pose().pushMatrix();\n        this.pose().scale(scale, scale);\n\n        switch (anchorPoint) {\n            case TOP_RIGHT -> x -= textRenderer.width(text) * scale;\n            case BOTTOM_LEFT -> y -= textRenderer.lineHeight * scale;\n            case BOTTOM_RIGHT -> {\n                x -= textRenderer.width(text) * scale;\n                y -= textRenderer.lineHeight * scale;\n            }\n        }\n\n\n        this.drawString(textRenderer, text, (int) (x * (1 / scale)), (int) (y * (1 / scale)), color, false);\n        this.pose().popMatrix();\n    }\n\n    public enum TextAnchor {\n        TOP_RIGHT, BOTTOM_RIGHT, TOP_LEFT, BOTTOM_LEFT\n    }\n\n    public void drawLine(int x1, int y1, int x2, int y2, double thiccness, Color color) {\n        drawLine(RenderPipelines.GUI, x1, y1, x2, y2, thiccness, color);\n    }\n\n    public void drawLine(RenderPipeline pipeline, int x1, int y1, int x2, int y2, double thiccness, Color color) {\n        this.guiRenderState.submitGuiElement(new LineElementRenderState(\n            pipeline,\n            new Matrix3x2f(this.pose()),\n            this.scissorStack.peek(),\n            x1, y1, x2, y2,\n            thiccness,\n            color\n        ));\n    }\n\n    public void drawCircle(int centerX, int centerY, int segments, double radius, Color color) {\n        drawCircle(OwoUIPipelines.GUI_TRIANGLE_FAN, centerX, centerY, segments, radius, color);\n    }\n\n    public void drawCircle(int centerX, int centerY, double angleFrom, double angleTo, int segments, double radius, Color color) {\n        drawCircle(OwoUIPipelines.GUI_TRIANGLE_FAN, centerX, centerY, angleFrom, angleTo, segments, radius, color);\n    }\n\n    public void drawCircle(RenderPipeline pipeline, int centerX, int centerY, int segments, double radius, Color color) {\n        drawCircle(pipeline, centerX, centerY, 0, 360, segments, radius, color);\n    }\n\n    public void drawCircle(RenderPipeline pipeline, int centerX, int centerY, double angleFrom, double angleTo, int segments, double radius, Color color) {\n        Preconditions.checkArgument(angleFrom < angleTo, \"angleFrom must be less than angleTo\");\n\n        this.guiRenderState.submitGuiElement(new CircleElementRenderState(\n            pipeline,\n            new Matrix3x2f(this.pose()),\n            this.scissorStack.peek(),\n            centerX, centerY, angleFrom, angleTo, segments, radius, color\n        ));\n    }\n\n    public void drawRing(int centerX, int centerY, int segments, double innerRadius, double outerRadius, Color innerColor, Color outerColor) {\n        drawRing(OwoUIPipelines.GUI_TRIANGLE_STRIP, centerX, centerY, segments, innerRadius, outerRadius, innerColor, outerColor);\n    }\n\n    public void drawRing(int centerX, int centerY, double angleFrom, double angleTo, int segments, double innerRadius, double outerRadius, Color innerColor, Color outerColor) {\n        drawRing(OwoUIPipelines.GUI_TRIANGLE_STRIP, centerX, centerY, angleFrom, angleTo, segments, innerRadius, outerRadius, innerColor, outerColor);\n    }\n\n    public void drawRing(RenderPipeline pipeline, int centerX, int centerY, int segments, double innerRadius, double outerRadius, Color innerColor, Color outerColor) {\n        drawRing(pipeline, centerX, centerY, 0d, 360d, segments, innerRadius, outerRadius, innerColor, outerColor);\n    }\n\n    public void drawRing(RenderPipeline pipeline, int centerX, int centerY, double angleFrom, double angleTo, int segments, double innerRadius, double outerRadius, Color innerColor, Color outerColor) {\n        Preconditions.checkArgument(angleFrom < angleTo, \"angleFrom must be less than angleTo\");\n        Preconditions.checkArgument(innerRadius < outerRadius, \"innerRadius must be less than outerRadius\");\n\n        this.guiRenderState.submitGuiElement(new RingElementRenderState(\n            pipeline,\n            new Matrix3x2f(this.pose()),\n            this.scissorStack.peek(),\n            centerX, centerY, angleFrom, angleTo, segments, innerRadius, outerRadius, innerColor, outerColor\n        ));\n    }\n\n    public void drawTooltip(Font textRenderer, int x, int y, List<ClientTooltipComponent> components) {\n        drawTooltip(textRenderer, x, y, components, null);\n    }\n\n    public void drawTooltip(Font textRenderer, int x, int y, List<ClientTooltipComponent> components, @Nullable Identifier texture) {\n        ((GuiGraphicsAccessor) this).owo$drawTooltipImmediately(textRenderer, components, x, y, DefaultTooltipPositioner.INSTANCE, texture);\n    }\n\n    @Override\n    protected void setTooltipForNextFrameInternal(Font textRenderer, List<ClientTooltipComponent> components, int x, int y, ClientTooltipPositioner positioner, @Nullable Identifier texture, boolean focused) {\n        super.setTooltipForNextFrameInternal(textRenderer, components, x, y, positioner, texture, focused);\n        this.setTooltipDrawer.accept(((GuiGraphicsAccessor) this).owo$getDeferredTooltip());\n    }\n\n    // --- debug rendering ---\n\n    public static void drawInsets(OwoUIGraphics self, int x, int y, int width, int height, Insets insets, int color) {\n        drawInsets(self, RenderPipelines.GUI, x, y, width, height, insets, color);\n    }\n\n    /**\n     * Draw the area around the given rectangle which\n     * the given insets describe\n     *\n     * @param x      The x-coordinate of top-left corner of the rectangle\n     * @param y      The y-coordinate of top-left corner of the rectangle\n     * @param width  The width of the rectangle\n     * @param height The height of the rectangle\n     * @param insets The insets to draw around the rectangle\n     * @param color  The color to draw the inset area with\n     */\n    public static void drawInsets(OwoUIGraphics self, RenderPipeline pipeline, int x, int y, int width, int height, Insets insets, int color) {\n        self.fill(pipeline, x - insets.left(), y - insets.top(), x + width + insets.right(), y, color);\n        self.fill(pipeline, x - insets.left(), y + height, x + width + insets.right(), y + height + insets.bottom(), color);\n\n        self.fill(pipeline, x - insets.left(), y, x, y + height, color);\n        self.fill(pipeline, x + width, y, x + width + insets.right(), y + height, color);\n    }\n\n    /**\n     * Draw the element inspector for the given tree, detailing the position,\n     * bounding box, margins and padding of each component\n     *\n     * @param root        The root component of the hierarchy to draw\n     * @param mouseX      The x-coordinate of the mouse pointer\n     * @param mouseY      The y-coordinate of the mouse pointer\n     * @param onlyHovered Whether to only draw the inspector for the hovered widget\n     */\n    public static void drawInspector(OwoUIGraphics self, ParentUIComponent root, double mouseX, double mouseY, boolean onlyHovered) {\n        var client = Minecraft.getInstance();\n        var textRenderer = client.font;\n\n        var children = new ArrayList<UIComponent>();\n        if (!onlyHovered) {\n            root.collectDescendants(children);\n        } else if (root.childAt((int) mouseX, (int) mouseY) != null) {\n            children.add(root.childAt((int) mouseX, (int) mouseY));\n        }\n\n        var pipeline = RenderPipelines.GUI;\n\n        for (var child : children) {\n            if (child instanceof ParentUIComponent parentComponent) {\n                drawInsets(self, pipeline, parentComponent.x(), parentComponent.y(), parentComponent.width(),\n                    parentComponent.height(), parentComponent.padding().get().inverted(), 0xA70CECDD);\n            }\n\n            final var margins = child.margins().get();\n            drawInsets(self, pipeline, child.x(), child.y(), child.width(), child.height(), margins, 0xA7FFF338);\n            self.drawRectOutline(pipeline, child.x(), child.y(), child.width(), child.height(), 0xFF3AB0FF);\n\n            if (onlyHovered) {\n\n                int inspectorX = child.x() + 1;\n                int inspectorY = child.y() + child.height() + child.margins().get().bottom() + 1;\n\n                final var message = Component.literal(child.getClass().getSimpleName())\n                    .append(child.id() == null ? \"\\n\" : \" '\" + child.id() + \"'\\n\")\n                    .append(child.inspectorDescriptor());\n                final var wrappedMessage = textRenderer.split(message, client.getWindow().getGuiScaledWidth() + 4);\n                int inspectorWidth = wrappedMessage.stream().mapToInt(textRenderer::width).max().orElse(30);\n                int inspectorHeight = textRenderer.lineHeight * wrappedMessage.size() + 4;\n\n                if (inspectorY > client.getWindow().getGuiScaledHeight() - inspectorHeight) {\n                    inspectorY -= child.fullSize().height() + inspectorHeight + 1;\n                    if (child instanceof ParentUIComponent parentComponent) {\n                        inspectorX += parentComponent.padding().get().left();\n                        inspectorY += parentComponent.padding().get().top();\n                    }\n                }\n                if (inspectorY < 0) inspectorY = 1;\n\n                if (inspectorX > client.getWindow().getGuiScaledWidth() - inspectorWidth) {\n                    inspectorX = client.getWindow().getGuiScaledWidth() - inspectorWidth - 2;\n                }\n                if (inspectorX < 0) inspectorX = 1;\n\n                self.fill(pipeline, inspectorX, inspectorY, inspectorX + inspectorWidth + 3, inspectorY + inspectorHeight, 0xA7000000);\n                self.drawRectOutline(pipeline, inspectorX, inspectorY, inspectorWidth + 3, inspectorHeight, 0xA7000000);\n\n                self.drawWordWrap(textRenderer, message, inspectorX + 2, inspectorY + 2, inspectorWidth, 0xFFFFFFFF, false);\n            }\n        }\n    }\n\n    public static class UtilityScreen extends Screen {\n\n        private static UtilityScreen INSTANCE;\n\n        private UtilityScreen() {\n            super(Component.empty());\n        }\n\n        public static UtilityScreen get() {\n            if (INSTANCE == null) {\n                INSTANCE = new UtilityScreen();\n\n                final var client = Minecraft.getInstance();\n                INSTANCE.init(\n                    client.getWindow().getGuiScaledWidth(),\n                    client.getWindow().getGuiScaledHeight()\n                );\n            }\n\n            return INSTANCE;\n        }\n\n        public boolean handleTextClick(Style style, Screen screenAfterRun) {\n            if (style.getClickEvent() == null) return false;\n            defaultHandleGameClickEvent(style.getClickEvent(), this.minecraft, screenAfterRun);\n\n            return true;\n        }\n\n        static {\n            WindowResizeCallback.EVENT.register((client, window) -> {\n                if (INSTANCE == null) return;\n                INSTANCE.init(window.getGuiScaledWidth(), window.getGuiScaledHeight());\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/OwoUIPipelines.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport com.mojang.blaze3d.pipeline.BlendFunction;\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.shaders.UniformType;\nimport com.mojang.blaze3d.vertex.DefaultVertexFormat;\nimport com.mojang.blaze3d.vertex.VertexFormat;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.ApiStatus;\n\npublic final class OwoUIPipelines {\n\n    public static final RenderPipeline.Snippet HSV_SNIPPET = RenderPipeline.builder(RenderPipelines.MATRICES_PROJECTION_SNIPPET)\n        .withVertexShader(Identifier.withDefaultNamespace(\"core/gui\"))\n        .withFragmentShader(Identifier.fromNamespaceAndPath(\"owo\", \"core/spectrum\"))\n        .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.QUADS)\n        .withBlend(BlendFunction.TRANSLUCENT)\n        .buildSnippet();\n\n    public static final RenderPipeline GUI_HSV = RenderPipeline.builder(HSV_SNIPPET)\n        .withLocation(Identifier.fromNamespaceAndPath(\"owo\", \"pipeline/gui_hsv\"))\n        .build();\n\n    public static final RenderPipeline GUI_BLUR = RenderPipeline.builder(RenderPipelines.MATRICES_PROJECTION_SNIPPET)\n        .withLocation(Identifier.fromNamespaceAndPath(\"owo\", \"pipeline/gui_blur\"))\n        .withVertexFormat(DefaultVertexFormat.POSITION, VertexFormat.Mode.QUADS)\n        .withVertexShader(Identifier.fromNamespaceAndPath(\"owo\", \"core/blur\"))\n        .withFragmentShader(Identifier.fromNamespaceAndPath(\"owo\", \"core/blur\"))\n        .withSampler(\"InputSampler\")\n        .withUniform(\"BlurSettings\", UniformType.UNIFORM_BUFFER)\n        .build();\n\n    public static final RenderPipeline GUI_TRIANGLE_FAN = RenderPipeline.builder(RenderPipelines.GUI_SNIPPET)\n        .withLocation(Identifier.fromNamespaceAndPath(\"owo\", \"pipeline/gui_triangle_fan\"))\n        .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.TRIANGLE_FAN)\n        .build();\n\n    public static final RenderPipeline GUI_TRIANGLE_STRIP = RenderPipeline.builder(RenderPipelines.GUI_SNIPPET)\n        .withLocation(Identifier.fromNamespaceAndPath(\"owo\", \"pipeline/gui_triangle_strip\"))\n        .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.TRIANGLE_STRIP)\n        .build();\n\n    public static final RenderPipeline GUI_TEXTURED_NO_BLEND = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)\n        .withLocation(Identifier.fromNamespaceAndPath(\"owo\", \"pipeline/gui_textured\"))\n        .withoutBlend()\n        .build();\n\n    @ApiStatus.Internal\n    public static void register() {\n        RenderPipelines.register(GUI_HSV);\n        RenderPipelines.register(GUI_BLUR);\n        RenderPipelines.register(GUI_TRIANGLE_FAN);\n        RenderPipelines.register(GUI_TRIANGLE_STRIP);\n        RenderPipelines.register(GUI_TEXTURED_NO_BLEND);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/ParentUIComponent.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.ui.parsing.IncompatibleUIModelException;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.chat.Style;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\n\npublic interface ParentUIComponent extends UIComponent {\n\n    /**\n     * Recalculate the layout of this component\n     */\n    void layout(Size space);\n\n    /**\n     * Called when a child of this parent component has been mutated in some way\n     * that would affect the layout of this component\n     *\n     * @param child The child that has been mutated\n     */\n    void onChildMutated(UIComponent child);\n\n    /**\n     * Queue a task to be run after the\n     * entire UI has finished updating\n     *\n     * @param task The task to run\n     */\n    void queue(Runnable task);\n\n    /**\n     * Set how this component should arrange its children\n     *\n     * @param horizontalAlignment The horizontal alignment method to use\n     * @param verticalAlignment   The vertical alignment method to use\n     */\n    default ParentUIComponent alignment(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) {\n        this.horizontalAlignment(horizontalAlignment);\n        this.verticalAlignment(verticalAlignment);\n        return this;\n    }\n\n    /**\n     * Set how this component should vertically arrange its children\n     *\n     * @param alignment The new alignment method to use\n     */\n    ParentUIComponent verticalAlignment(VerticalAlignment alignment);\n\n    /**\n     * @return How this component vertically arranges its children\n     */\n    VerticalAlignment verticalAlignment();\n\n    /**\n     * Set how this component should horizontally arrange its children\n     *\n     * @param alignment The new alignment method to use\n     */\n    ParentUIComponent horizontalAlignment(HorizontalAlignment alignment);\n\n    /**\n     * @return How this component horizontally arranges its children\n     */\n    HorizontalAlignment horizontalAlignment();\n\n    /**\n     * Set the internal padding of this component\n     *\n     * @param padding The new padding to use\n     */\n    ParentUIComponent padding(Insets padding);\n\n    /**\n     * @return The internal padding of this component\n     */\n    AnimatableProperty<Insets> padding();\n\n    /**\n     * Set if this component should let its children overflow\n     * its bounding box\n     *\n     * @param allowOverflow {@code true} if this component should let\n     *                      its children overflow its bounding box\n     */\n    ParentUIComponent allowOverflow(boolean allowOverflow);\n\n    /**\n     * @return {@code true} if this component allows its\n     * children to overflow its bounding box\n     */\n    boolean allowOverflow();\n\n    /**\n     * Set the surface this component uses\n     *\n     * @param surface The new surface to use\n     */\n    ParentUIComponent surface(Surface surface);\n\n    /**\n     * @return The surface this component currently uses\n     */\n    Surface surface();\n\n    /**\n     * @return The children of this component\n     */\n    List<UIComponent> children();\n\n    /**\n     * Remove the given child from this component\n     */\n    ParentUIComponent removeChild(UIComponent child);\n\n    @Override\n    default void drawTooltip(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {\n        if (this.hasParent()) {\n            UIComponent.super.drawTooltip(context, mouseX, mouseY, partialTicks, delta);\n            return;\n        }\n\n        var hoveredDescendants = new ArrayList<UIComponent>();\n        this.forEachDescendantWhere(hoveredDescendants::add, component -> component.isInBoundingBox(mouseX, mouseY));\n        hoveredDescendants.remove(this);\n\n        for (int i = hoveredDescendants.size() - 1; i >= 0; i--) {\n            ParentUIComponent nextParent = null;\n            for (int parentIdx = i - 1; parentIdx >= 0; parentIdx--) {\n                if (hoveredDescendants.get(parentIdx) instanceof ParentUIComponent parent) {\n                    nextParent = parent;\n                    break;\n                }\n            }\n\n            var current = hoveredDescendants.get(i);\n            if (nextParent != null && current.parent() != nextParent) break;\n            if (!current.shouldDrawTooltip(mouseX, mouseY)) continue;\n\n            context.push();\n            for (; i >= 0; i--) {\n                if (i > 0 && hoveredDescendants.get(i).parent() != hoveredDescendants.get(i - 1)) break;\n                context.translate(0, 0);\n            }\n\n            current.drawTooltip(context, mouseX, mouseY, partialTicks, delta);\n            context.pop();\n\n            break;\n        }\n    }\n\n    @Override\n    default boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        var iter = this.children().listIterator(this.children().size());\n\n        while (iter.hasPrevious()) {\n            var child = iter.previous();\n            if (!child.isInBoundingBox(this.x() + click.x(), this.y() + click.y())) continue;\n            if (child.onMouseDown(new MouseButtonEvent(this.x() + click.x() - child.x(), this.y() + click.y() - child.y(), click.buttonInfo()), doubled)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    @Override\n    default boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        var iter = this.children().listIterator(this.children().size());\n\n        while (iter.hasPrevious()) {\n            var child = iter.previous();\n            if (!child.isInBoundingBox(this.x() + mouseX, this.y() + mouseY)) continue;\n            if (child.onMouseScroll(this.x() + mouseX - child.x(), this.y() + mouseY - child.y(), amount)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * @apiNote When overriding update and calling {@code ParentComponent.super.update()},\n     * ensure that {@link UIComponent#update(float, int, int)} is called as well, through some means\n     */\n    @Override\n    default void update(float delta, int mouseX, int mouseY) {\n        this.padding().update(delta);\n\n        for (int i = 0; i < this.children().size(); i++) {\n            this.children().get(i).update(delta, mouseX, mouseY);\n        }\n    }\n\n    @Override\n    default void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        UIComponent.super.parseProperties(model, element, children);\n        UIParsing.apply(children, \"padding\", Insets::parse, this::padding);\n        UIParsing.apply(children, \"surface\", Surface::parse, this::surface);\n        UIParsing.apply(children, \"vertical-alignment\", VerticalAlignment::parse, this::verticalAlignment);\n        UIParsing.apply(children, \"horizontal-alignment\", HorizontalAlignment::parse, this::horizontalAlignment);\n        UIParsing.apply(children, \"allow-overflow\", UIParsing::parseBool, this::allowOverflow);\n    }\n\n    @Override\n    default MutableComponent inspectorDescriptor() {\n        final var padding = this.padding().get();\n        return UIComponent.super.inspectorDescriptor().append(\n                Component.literal(\" >\" + padding.top() + \",\" + padding.bottom() + \",\" + padding.left() + \",\" + padding.right() + \"<\")\n                        .setStyle(Style.EMPTY.withColor(ChatFormatting.AQUA))\n        );\n    }\n\n    /**\n     * Recursively find the child with the given id in the\n     * hierarchy below this component\n     *\n     * @param id The id to search for\n     * @return The child with the given id, or {@code null} if\n     * none was found\n     */\n    @SuppressWarnings(\"unchecked\")\n    default <T extends UIComponent> T childById(@NotNull Class<T> expectedClass, @NotNull String id) {\n        var iter = this.children().listIterator(this.children().size());\n\n        while (iter.hasPrevious()) {\n            var child = iter.previous();\n            if (Objects.equals(child.id(), id)) {\n\n                if (!expectedClass.isAssignableFrom(child.getClass())) {\n                    throw new IncompatibleUIModelException(\n                            \"Expected child with id '\" + id + \"'\"\n                                    + \" to be a \" + expectedClass.getSimpleName()\n                                    + \" but it is a \" + child.getClass().getSimpleName()\n                    );\n                }\n\n                return (T) child;\n            } else if (child instanceof ParentUIComponent parent) {\n                var candidate = parent.childById(expectedClass, id);\n                if (candidate != null) return candidate;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Get the most specific child at the given coordinates\n     *\n     * @param x The x-coordinate to query\n     * @param y The y-coordinate to query\n     * @return The most specific child at the given coordinates,\n     * or {@code null} if there is none\n     */\n    default @Nullable UIComponent childAt(int x, int y) {\n        var iter = this.children().listIterator(this.children().size());\n\n        while (iter.hasPrevious()) {\n            var child = iter.previous();\n            if (child.isInBoundingBox(x, y)) {\n                if (child instanceof ParentUIComponent parent) {\n                    return parent.childAt(x, y);\n                } else {\n                    return child;\n                }\n            }\n        }\n\n        return this.isInBoundingBox(x, y) ? this : null;\n    }\n\n    /**\n     * Collect the entire component hierarchy below the given component\n     * into the given list\n     *\n     * @param into The list into which to collect the hierarchy\n     */\n    default void collectDescendants(ArrayList<UIComponent> into) {\n        this.forEachDescendant(into::add);\n    }\n\n    /**\n     * Run the given callback function for every\n     * descendant of this component\n     *\n     * @param action The action to execute for each descendant\n     */\n    default void forEachDescendant(Consumer<UIComponent> action) {\n        action.accept(this);\n        for (var child : this.children()) {\n            if (child instanceof ParentUIComponent parent) {\n                parent.forEachDescendant(action);\n            } else {\n                action.accept(child);\n            }\n        }\n    }\n\n    /**\n     * Run the given callback function on every\n     * descendant of this component for which {@code condition}\n     * is true\n     *\n     * @param action The action to execute for each descendant\n     */\n    default void forEachDescendantWhere(Consumer<UIComponent> action, Predicate<UIComponent> condition) {\n        action.accept(this);\n        for (var child : this.children()) {\n            if (!condition.test(child)) continue;\n\n            if (child instanceof ParentUIComponent parent) {\n                parent.forEachDescendantWhere(action, condition);\n            } else {\n                action.accept(child);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/PositionedRectangle.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport net.minecraft.util.Mth;\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2f;\n\n/**\n * Represents a rectangle positioned in 2D-space\n */\npublic interface PositionedRectangle extends Animatable<PositionedRectangle> {\n\n    /**\n     * @return The x-coordinate of the top-left corner of this rectangle\n     */\n    int x();\n\n    /**\n     * @return The y-coordinate of the top-left corner of this rectangle\n     */\n    int y();\n\n    /**\n     * @return The width of this rectangle\n     */\n    int width();\n\n    /**\n     * @return The height of this rectangle\n     */\n    int height();\n\n    /**\n     * @return {@code true} if this rectangle contains the given point\n     */\n    default boolean isInBoundingBox(double x, double y) {\n        return x >= this.x() && x < this.x() + this.width() && y >= this.y() && y < this.y() + this.height();\n    }\n\n    default boolean intersects(PositionedRectangle other) {\n        return other.x() < this.x() + this.width()\n                && other.x() + other.width() >= this.x()\n                && other.y() < this.y() + this.height()\n                && other.y() + other.height() >= this.y();\n    }\n\n    default PositionedRectangle intersection(PositionedRectangle other) {\n\n        // my brain is fucking dead on the floor\n        // this code is really, really simple\n        // and honestly quite obvious\n        //\n        // my brain did not agree\n        // glisco, 2022\n\n        int leftEdge = Math.max(this.x(), other.x());\n        int topEdge = Math.max(this.y(), other.y());\n\n        int rightEdge = Math.min(this.x() + this.width(), other.x() + other.width());\n        int bottomEdge = Math.min(this.y() + this.height(), other.y() + other.height());\n\n        return of(\n                leftEdge,\n                topEdge,\n                Math.max(rightEdge - leftEdge, 0),\n                Math.max(bottomEdge - topEdge, 0)\n        );\n    }\n\n    @Override\n    default PositionedRectangle interpolate(PositionedRectangle next, float delta) {\n        return PositionedRectangle.of(\n                (int) Mth.lerpInt(delta, this.x(), next.x()),\n                (int) Mth.lerpInt(delta, this.y(), next.y()),\n                (int) Mth.lerpInt(delta, this.width(), next.width()),\n                (int) Mth.lerpInt(delta, this.height(), next.height())\n        );\n    }\n\n    default PositionedRectangle transform(Matrix3x2f matrix) {\n        var pos1 = matrix.transformPosition(x(), y(), new Vector2f());\n        var pos2 = matrix.transformPosition(x() + width(), y() + height(), new Vector2f());\n\n        return PositionedRectangle.of((int) pos1.x, (int) pos1.y, (int) (pos2.x - pos1.x), (int) (pos2.y - pos1.y));\n    }\n\n    static PositionedRectangle of(int x, int y, Size size) {\n        return of(x, y, size.width(), size.height());\n    }\n\n    static PositionedRectangle of(int x, int y, int width, int height) {\n        return new PositionedRectangle() {\n            @Override\n            public int x() {\n                return x;\n            }\n\n            @Override\n            public int y() {\n                return y;\n            }\n\n            @Override\n            public int width() {\n                return width;\n            }\n\n            @Override\n            public int height() {\n                return height;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Positioning.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport net.minecraft.util.Mth;\nimport org.w3c.dom.Element;\n\nimport java.util.Locale;\nimport java.util.Objects;\n\npublic class Positioning implements Animatable<Positioning> {\n\n    private static final Positioning LAYOUT_POSITIONING = new Positioning(0, 0, Type.LAYOUT);\n\n    public final Type type;\n    public final int x, y;\n\n    private Positioning(int x, int y, Type type) {\n        this.type = type;\n        this.x = x;\n        this.y = y;\n    }\n\n    public Positioning withX(int x) {\n        return new Positioning(x, this.y, this.type);\n    }\n\n    public Positioning withY(int y) {\n        return new Positioning(this.x, y, this.type);\n    }\n\n    public boolean isRelative() {\n        return this.type == Type.RELATIVE || this.type == Type.ACROSS;\n    }\n\n    @Override\n    public Positioning interpolate(Positioning next, float delta) {\n        if (next.type != this.type) {\n            Owo.LOGGER.warn(\"Cannot interpolate between positioning of type \" + this.type + \" and \" + next.type);\n            return this;\n        }\n\n        return new Positioning(\n                Mth.lerpInt(delta, this.x, next.x),\n                Mth.lerpInt(delta, this.y, next.y),\n                this.type\n        );\n    }\n\n    /**\n     * Position the component at an absolute offset\n     * from the root of parent\n     *\n     * @param xPixels The offset on the x-axis\n     * @param yPixels The offset on the y-axis\n     */\n    public static Positioning absolute(int xPixels, int yPixels) {\n        return new Positioning(xPixels, yPixels, Type.ABSOLUTE);\n    }\n\n    /**\n     * Position the component at a relative offset\n     * inside the parent. This respect the size of\n     * the component itself. As such:\n     * <ul>\n     *     <li>50,50 centers the component inside the parent</li>\n     *     <li>100,50 centers to component vertically and pushes it all the way to the right</li>\n     *     <li>100,100 pushes the component all the way into the bottom right corner of the parent</li>\n     * </ul>\n     *\n     * @param xPercent The offset on the x-axis\n     * @param yPercent The offset on the y-axis\n     */\n    public static Positioning relative(int xPercent, int yPercent) {\n        return new Positioning(xPercent, yPercent, Type.RELATIVE);\n    }\n\n    /**\n     * Position the component the specified percentage\n     * across the parent, <i>not including the component's own size</i>\n     *\n     * @param xPercent The offset on the x-axis\n     * @param yPercent The offset on the y-axis\n     */\n    public static Positioning across(int xPercent, int yPercent) {\n        return new Positioning(xPercent, yPercent, Type.ACROSS);\n    }\n\n    /**\n     * Position the component using whatever layout\n     * method the parent component wants to apply\n     */\n    public static Positioning layout() {\n        return LAYOUT_POSITIONING;\n    }\n\n    public enum Type {\n        RELATIVE, ACROSS, ABSOLUTE, LAYOUT\n    }\n\n    public static Positioning parse(Element positioningElement) {\n        var typeString = positioningElement.getAttribute(\"type\");\n        if (typeString.isBlank()) {\n            throw new UIModelParsingException(\"Missing 'type' attribute on positioning declaration. Must be one of: relative, absolute, layout\");\n        }\n\n        var type = Type.valueOf(typeString.toUpperCase(Locale.ROOT));\n\n        var values = positioningElement.getTextContent().strip();\n        if (!values.matches(\"-?\\\\d+,-?\\\\d+\")) {\n            throw new UIModelParsingException(\"Invalid value in positioning declaration\");\n        }\n\n        int x = Integer.parseInt(values.split(\",\")[0]);\n        int y = Integer.parseInt(values.split(\",\")[1]);\n\n        return new Positioning(x, y, type);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Positioning that = (Positioning) o;\n        return x == that.x && y == that.y && type == that.type;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(type, x, y);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Size.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport org.jetbrains.annotations.ApiStatus;\n\n/**\n * Represents a two-dimensional value, used for\n * describing position-less rectangles in 2D-space\n *\n * @param width  The width of the rectangle\n * @param height The height of the rectangle\n */\npublic record Size(int width, int height) {\n\n    public static final Endec<Size> ENDEC =StructEndecBuilder.of(\n            Endec.INT.fieldOf(\"width\", Size::width),\n            Endec.INT.fieldOf(\"height\", Size::height),\n            Size::of\n    );\n\n    private static final Size ZERO = new Size(0, 0);\n\n    @ApiStatus.Internal\n    @Deprecated(forRemoval = true)\n    public Size {}\n\n    public static Size of(int width, int height) {\n        return new Size(width, height);\n    }\n\n    public static Size square(int sideLength) {\n        return new Size(sideLength, sideLength);\n    }\n\n    /**\n     * @return A size with both values equal to 0\n     */\n    public static Size zero() {\n        return ZERO;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Sizing.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport net.minecraft.util.Mth;\nimport org.w3c.dom.Element;\n\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.function.Function;\n\npublic class Sizing implements Animatable<Sizing> {\n\n    private static final Sizing CONTENT_SIZING = new Sizing(0, Method.CONTENT);\n\n    public final Method method;\n    public final int value;\n\n    private Sizing(int value, Method method) {\n        this.method = method;\n        this.value = value;\n    }\n\n    /**\n     * Inflate into the given space\n     *\n     * @param space               The available space\n     * @param contentSizeFunction A function for making the component set the\n     *                            size based on its content\n     */\n    public int inflate(int space, Function<Sizing, Integer> contentSizeFunction) {\n        return switch (this.method) {\n            case FIXED -> this.value;\n            case FILL, EXPAND -> Math.round((this.value / 100f) * space);\n            case CONTENT -> contentSizeFunction.apply(this) + this.value * 2;\n        };\n    }\n\n    public static Sizing fixed(int value) {\n        return new Sizing(value, Method.FIXED);\n    }\n\n    /**\n     * Dynamically size the component based on its content,\n     * without any padding\n     */\n    public static Sizing content() {\n        return CONTENT_SIZING;\n    }\n\n    /**\n     * Dynamically size the component based on its content\n     *\n     * @param padding Padding to add onto the size of the content\n     */\n    public static Sizing content(int padding) {\n        return new Sizing(padding, Method.CONTENT);\n    }\n\n    /**\n     * Dynamically size the component to fill the available space\n     */\n    public static Sizing fill() {\n        return fill(100);\n    }\n\n    /**\n     * Dynamically size the component based on the available space\n     *\n     * @param percent How many percent of the available space to take up\n     */\n    public static Sizing fill(int percent) {\n        return new Sizing(percent, Method.FILL);\n    }\n\n    /**\n     * Dynamically size the component based on the remaining space\n     * <i>after all other components have been laid out</i>\n     */\n    public static Sizing expand() {\n        return expand(100);\n    }\n\n    /**\n     * Dynamically size the component based on the remaining space\n     * <i>after all other components have been laid out</i>\n     *\n     * @param percent How many percent of the available space to take up\n     */\n    public static Sizing expand(int percent) {\n        return new Sizing(percent, Method.EXPAND);\n    }\n\n    /**\n     * A collection of utility methods for generating random sizing instances\n     *\n     * @author chyzman\n     */\n    public static class Random {\n        private static final java.util.Random SIZING_RANDOM = new java.util.Random();\n\n        /**\n         * Generate a random fill sizing instance with a value between {@code min} and {@code max}\n         *\n         * @param min The minimum value\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing fill(int min, int max) {\n            return Sizing.fill(SIZING_RANDOM.nextInt(min, max));\n        }\n\n        /**\n         * Generate a random fill sizing instance with a value between 0 and {@code max}\n         *\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing fill(int max) {\n            return Sizing.fill(SIZING_RANDOM.nextInt(0, max));\n        }\n\n        /**\n         * Generate a random fill sizing instance with a value between 0 and 100\n         *\n         * @return A random sizing instance\n         */\n        public static Sizing fill() {\n            return Sizing.fill(SIZING_RANDOM.nextInt(0, 100));\n        }\n\n        /**\n         * Generate a random expand sizing instance with a value between {@code min} and {@code max}\n         *\n         * @param min The minimum value\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing expand(int min, int max) {\n            return Sizing.expand(SIZING_RANDOM.nextInt(min, max));\n        }\n\n        /**\n         * Generate a random expand sizing instance with a value between 0 and {@code max}\n         *\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing expand(int max) {\n            return Sizing.expand(SIZING_RANDOM.nextInt(0, max));\n        }\n\n        /**\n         * Generate a random expand sizing instance with a value between 0 and 100\n         *\n         * @return A random sizing instance\n         */\n        public static Sizing expand() {\n            return Sizing.expand(SIZING_RANDOM.nextInt(0, 100));\n        }\n\n        /**\n         * Generate a random fixed sizing instance with a value between {@code min} and {@code max}\n         *\n         * @param min The minimum value\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing fixed(int min, int max) {\n            return Sizing.fixed(SIZING_RANDOM.nextInt(min, max));\n        }\n\n        /**\n         * Generate a random fixed sizing instance with a value between 0 and {@code max}\n         *\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing fixed(int max) {\n            return Sizing.fixed(SIZING_RANDOM.nextInt(0, max));\n        }\n\n        /**\n         * Generate a random fixed sizing instance with a value between 0 and 100\n         *\n         * @return A random sizing instance\n         */\n        public static Sizing fixed() {\n            return Sizing.fixed(SIZING_RANDOM.nextInt(0, 100));\n        }\n\n        /**\n         * Generate a random content sizing instance with a padding value between {@code min} and {@code max}\n         *\n         * @param min The minimum value\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing content(int min, int max) {\n            return Sizing.content(SIZING_RANDOM.nextInt(min, max));\n        }\n\n        /**\n         * Generate a random content sizing instance with a padding value between 0 and {@code max}\n         *\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing content(int max) {\n            return Sizing.content(SIZING_RANDOM.nextInt(0, max));\n        }\n\n        /**\n         * Generate a random content sizing instance with a padding value between 0 and 100\n         *\n         * @return A random sizing instance\n         */\n        public static Sizing content() {\n            return Sizing.content(SIZING_RANDOM.nextInt(0, 100));\n        }\n\n        /**\n         * Generate a random sizing instance with a value between {@code min} and {@code max}\n         *\n         * @param min The minimum value\n         * @param max The maximum value\n         * @return A random sizing instance\n         * @apiNote May crash if put on a component that doesn't support content sizing\n         */\n        public static Sizing random(int min, int max) {\n            return switch (SIZING_RANDOM.nextInt(0, 4)) {\n                case 0 -> fill(min, max);\n                case 1 -> expand(min, max);\n                case 2 -> fixed(min, max);\n                case 3 -> content(min, max);\n                default -> throw new IllegalStateException(\"Unexpected value: \" + SIZING_RANDOM.nextInt(0, 4));\n            };\n        }\n\n        /**\n         * Generate a random sizing instance with a value between 0 and {@code max}\n         *\n         * @param max The maximum value\n         * @return A random sizing instance\n         * @apiNote May crash if put on a component that doesn't support content sizing\n         */\n        public static Sizing random(int max) {\n            return random(0, max);\n        }\n\n        /**\n         * Generate a random sizing instance with a value between 0 and 100\n         *\n         * @return A random sizing instance\n         * @apiNote May crash if put on a component that doesn't support content sizing\n         */\n        public static Sizing random() {\n            return random(0, 100);\n        }\n\n        /**\n         * Generate a random sizing instance with a value between {@code min} and {@code max}\n         * that is not content-based\n         *\n         * @param min The minimum value\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing noContent(int min, int max) {\n            return switch (SIZING_RANDOM.nextInt(0, 3)) {\n                case 0 -> fill(min, max);\n                case 1 -> expand(min, max);\n                case 2 -> fixed(min, max);\n                default -> throw new IllegalStateException(\"Unexpected value: \" + SIZING_RANDOM.nextInt(0, 3));\n            };\n        }\n\n        /**\n         * Generate a random sizing instance with a value between 0 and {@code max}\n         * that is not content-based\n         *\n         * @param max The maximum value\n         * @return A random sizing instance\n         */\n        public static Sizing noContent(int max) {\n            return noContent(0, max);\n        }\n\n        /**\n         * Generate a random sizing instance that is not content-based\n         *\n         * @return A random sizing instance\n         */\n        public static Sizing noContent() {\n            return noContent(0, 100);\n        }\n    }\n\n    /**\n     * @return {@code true} if this sizing instance\n     * uses the {@linkplain Method#CONTENT CONTENT} method\n     */\n    public boolean isContent() {\n        return this.method == Method.CONTENT;\n    }\n\n    /**\n     * @return {@code true} if this sizing instance\n     * uses the {@linkplain Method#EXPAND EXPAND} method\n     */\n    public boolean isExpand() {\n        return this.method == Method.EXPAND;\n    }\n\n    /**\n     * The content factor of a sizing instance describes where\n     * on the spectrum from content to fixed sizing it sits. Specifically, this is\n     * used to lerp the reference frame used for calculating {@code fill(...)} sizing\n     * on children between the available space in this component (content factor 0)\n     * and this component's own available space (content factor 1), both of which can be\n     * independently determined prior to layout calculations\n     */\n    public float contentFactor() {\n        return this.isContent() ? 1f : 0f;\n    }\n\n    @Override\n    public Sizing interpolate(Sizing next, float delta) {\n        if (next.method != this.method) {\n            return new MergedSizing(this, next, delta);\n        } else {\n            return new Sizing(Mth.lerpInt(delta, this.value, next.value), this.method);\n        }\n    }\n\n    public enum Method {\n        FIXED, CONTENT, FILL, EXPAND\n    }\n\n    public static Sizing parse(Element sizingElement) {\n        var methodString = sizingElement.getAttribute(\"method\");\n        if (methodString.isBlank()) {\n            throw new UIModelParsingException(\"Missing 'method' attribute on sizing declaration. Must be one of: fixed, content, fill\");\n        }\n\n        var method = Method.valueOf(methodString.toUpperCase(Locale.ROOT));\n        var value = sizingElement.getTextContent().strip();\n\n        if (method == Method.CONTENT) {\n            if (!value.matches(\"(-?\\\\d+)?\")) {\n                throw new UIModelParsingException(\"Invalid value in sizing declaration\");\n            }\n\n            return new Sizing(value.isEmpty() ? 0 : Integer.parseInt(value), method);\n        } else {\n            if (!value.matches(\"-?\\\\d+\")) {\n                throw new UIModelParsingException(\"Invalid value in sizing declaration\");\n            }\n\n            return new Sizing(Integer.parseInt(value), method);\n        }\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Sizing sizing = (Sizing) o;\n        return value == sizing.value && method == sizing.method;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(method, value);\n    }\n\n    private static final class MergedSizing extends Sizing {\n\n        private final Sizing first, second;\n        private final float delta;\n\n        private MergedSizing(Sizing first, Sizing second, float delta) {\n            super(first.value, first.method);\n            this.first = first;\n            this.second = second;\n            this.delta = delta;\n        }\n\n        @Override\n        public int inflate(int space, Function<Sizing, Integer> contentSizeFunction) {\n            return Mth.lerpInt(\n                    this.delta,\n                    this.first.inflate(space, contentSizeFunction),\n                    this.second.inflate(space, contentSizeFunction)\n            );\n        }\n\n        @Override\n        public Sizing interpolate(Sizing next, float delta) {\n            return this.first.interpolate(next, delta);\n        }\n\n        @Override\n        public boolean isContent() {\n            return this.first.isContent() || this.second.isContent();\n        }\n\n        @Override\n        public float contentFactor() {\n            if (this.first.isContent() && this.second.isContent()) return super.contentFactor();\n\n            if (this.first.isContent()) {\n                return 1f - delta;\n            } else if (this.second.isContent()) {\n                return delta;\n            } else {\n                return 0f;\n            }\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            if (!super.equals(o)) return false;\n            MergedSizing that = (MergedSizing) o;\n            return Float.compare(delta, that.delta) == 0 && Objects.equals(first, that.first) && Objects.equals(second, that.second);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(super.hashCode(), first, second, delta);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/Surface.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.ui.parsing.UIModelParsingException;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.renderstate.BlurQuadElementRenderState;\nimport io.wispforest.owo.ui.renderstate.CubeMapElementRenderState;\nimport io.wispforest.owo.ui.util.NinePatchTexture;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil;\nimport net.minecraft.client.renderer.PanoramaRenderer;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\npublic interface Surface {\n\n    Surface BLANK = (context, component) -> {};\n\n    Surface PANEL = (context, component) -> {\n        context.drawPanel(component.x(), component.y(), component.width(), component.height(), false);\n    };\n\n    Surface DARK_PANEL = (context, component) -> {\n        context.drawPanel(component.x(), component.y(), component.width(), component.height(), true);\n    };\n\n    Surface PANEL_INSET = (context, component) -> {\n        NinePatchTexture.draw(OwoUIGraphics.PANEL_INSET_NINE_PATCH_TEXTURE, context, component);\n    };\n\n    Surface VANILLA_TRANSLUCENT = (context, component) -> {\n        context.drawGradientRect(\n            component.x(), component.y(), component.width(), component.height(),\n            0xC0101010, 0xC0101010, 0xD0101010, 0xD0101010\n        );\n    };\n\n    Surface TOOLTIP = tooltip(null);\n\n    static Surface tooltip(@Nullable Identifier texture) {\n        return (context, component) -> {\n            TooltipRenderUtil.renderTooltipBackground(context, component.x() + 4, component.y() + 4, component.width() - 8, component.height() - 8, texture);\n        };\n    }\n\n    static Surface blur(float quality, float size) {\n        return (context, component) -> {\n            context.guiRenderState.submitGuiElement(new BlurQuadElementRenderState(\n                new Matrix3x2f(context.pose()),\n                new ScreenRectangle(component.x(), component.y(), component.width(), component.height()),\n                context.scissorStack.peek(),\n                16, quality, size\n            ));\n        };\n    }\n\n    static Surface optionsBackground() {\n        return Surface.vanillaPanorama(false).and(Surface.blur(5, 10));\n    }\n\n    static Surface vanillaPanorama(boolean alwaysVisible) {\n        return panorama(Minecraft.getInstance().gameRenderer.getPanorama(), alwaysVisible);\n    }\n\n    static Surface panorama(PanoramaRenderer renderer, boolean alwaysVisible) {\n        return (context, component) -> {\n            if (!alwaysVisible && Minecraft.getInstance().level != null) return;\n            context.guiRenderState.submitPicturesInPictureState(new CubeMapElementRenderState(\n                renderer, true,\n                new ScreenRectangle(component.x(), component.y(), component.width(), component.height()),\n                context.scissorStack.peek()\n            ));\n        };\n    }\n\n    static Surface flat(int color) {\n        return (context, component) -> context.fill(component.x(), component.y(), component.x() + component.width(), component.y() + component.height(), color);\n    }\n\n    static Surface outline(int color) {\n        return (context, component) -> context.drawRectOutline(component.x(), component.y(), component.width(), component.height(), color);\n    }\n\n    static Surface tiled(Identifier texture, int textureWidth, int textureHeight) {\n        return (context, component) -> {\n            context.blit(RenderPipelines.GUI_TEXTURED, texture, component.x(), component.y(), 0, 0, component.width(), component.height(), textureWidth, textureHeight);\n        };\n    }\n\n    static Surface panelWithInset(int insetWidth) {\n        return Surface.PANEL.and((context, component) -> {\n            NinePatchTexture.draw(\n                OwoUIGraphics.PANEL_INSET_NINE_PATCH_TEXTURE,\n                context,\n                component.x() + insetWidth,\n                component.y() + insetWidth,\n                component.width() - insetWidth * 2,\n                component.height() - insetWidth * 2\n            );\n        });\n    }\n\n    void draw(OwoUIGraphics context, ParentUIComponent component);\n\n    default Surface and(Surface surface) {\n        return (context, component) -> {\n            this.draw(context, component);\n            surface.draw(context, component);\n        };\n    }\n\n    static Surface parse(Element surfaceElement) {\n        var children = UIParsing.<Element>allChildrenOfType(surfaceElement, Node.ELEMENT_NODE);\n        var surface = BLANK;\n\n        for (var child : children) {\n            surface = switch (child.getNodeName()) {\n                case \"panel\" -> surface.and(child.getAttribute(\"dark\").equalsIgnoreCase(\"true\")\n                    ? DARK_PANEL\n                    : PANEL);\n                case \"tiled\" -> {\n                    UIParsing.expectAttributes(child, \"texture-width\", \"texture-height\");\n                    yield surface.and(tiled(\n                        UIParsing.parseIdentifier(child),\n                        UIParsing.parseUnsignedInt(child.getAttributeNode(\"texture-width\")),\n                        UIParsing.parseUnsignedInt(child.getAttributeNode(\"texture-height\")))\n                    );\n                }\n                case \"blur\" -> {\n                    UIParsing.expectAttributes(child, \"size\", \"quality\");\n                    yield surface.and(blur(\n                        UIParsing.parseFloat(child.getAttributeNode(\"quality\")),\n                        UIParsing.parseFloat(child.getAttributeNode(\"size\"))\n                    ));\n                }\n                case \"panel-with-inset\" -> surface.and(panelWithInset(UIParsing.parseUnsignedInt(child)));\n                case \"options-background\" -> surface.and(optionsBackground());\n                case \"vanilla-translucent\" -> surface.and(VANILLA_TRANSLUCENT);\n                case \"panel-inset\" -> surface.and(PANEL_INSET);\n                case \"tooltip\" -> surface.and(TOOLTIP);\n                case \"outline\" -> surface.and(outline(Color.parseAndPack(child)));\n                case \"flat\" -> surface.and(flat(Color.parseAndPack(child)));\n                default -> throw new UIModelParsingException(\"Unknown surface type '\" + child.getNodeName() + \"'\");\n            };\n        }\n\n        return surface;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/UIComponent.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport io.wispforest.owo.ui.event.*;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.parsing.UIParsing;\nimport io.wispforest.owo.ui.util.FocusHandler;\nimport io.wispforest.owo.util.EventSource;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.chat.Style;\nimport org.jetbrains.annotations.Contract;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Element;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic interface UIComponent extends PositionedRectangle {\n\n    /**\n     * Draw the current state of this component onto the screen\n     *\n     * @param graphics     The transformation stack\n     * @param mouseX       The mouse pointer's x-coordinate\n     * @param mouseY       The mouse pointer's y-coordinate\n     * @param partialTicks The fraction of the current tick that has passed\n     * @param delta        The duration of the last frame, in partial ticks\n     */\n    void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta);\n\n    /**\n     * Draw the current tooltip of this component onto the screen\n     *\n     * @param context      The transformation stack\n     * @param mouseX       The mouse pointer's x-coordinate\n     * @param mouseY       The mouse pointer's y-coordinate\n     * @param partialTicks The fraction of the current tick that has passed\n     * @param delta        The duration of the last frame, in partial ticks\n     */\n    default void drawTooltip(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {\n        if (!this.shouldDrawTooltip(mouseX, mouseY)) return;\n        context.drawTooltip(Minecraft.getInstance().font, mouseX, mouseY, this.tooltip());\n    }\n\n    /**\n     * Draw something which clearly indicates\n     * that this component is currently focused\n     *\n     * @param context      The transformation stack\n     * @param mouseX       The mouse pointer's x-coordinate\n     * @param mouseY       The mouse pointer's y-coordinate\n     * @param partialTicks The fraction of the current tick that has passed\n     * @param delta        The duration of the last frame, in partial ticks\n     */\n    default void drawFocusHighlight(OwoUIGraphics context, int mouseX, int mouseY, float partialTicks, float delta) {\n        context.drawRectOutline(this.x(), this.y(), this.width(), this.height(), 0xFFFFFFFF);\n    }\n\n    /**\n     * @return The parent of this component\n     */\n    @Contract(pure = true)\n    @Nullable ParentUIComponent parent();\n\n    /**\n     * @return The focus handler of this component hierarchy\n     */\n    @Contract(pure = true)\n    @Nullable FocusHandler focusHandler();\n\n    /**\n     * Update this component's positioning and notify the parent\n     *\n     * @param positioning The new positioning to use\n     * @return The component\n     */\n    UIComponent positioning(Positioning positioning);\n\n    /**\n     * @return The positioning of this component\n     */\n    @Contract(pure = true)\n    AnimatableProperty<Positioning> positioning();\n\n    /**\n     * Set the external margins of this component and notify the parent\n     *\n     * @param margins The new margins to use\n     */\n    UIComponent margins(Insets margins);\n\n    /**\n     * @return The external margins of this component\n     */\n    @Contract(pure = true)\n    AnimatableProperty<Insets> margins();\n\n    /**\n     * Set the method this component uses to determine its size\n     * per axis\n     *\n     * @param horizontalSizing The new sizing method to use on the x-axis\n     * @param verticalSizing   The new sizing method to use on the y-axis\n     */\n    default UIComponent sizing(Sizing horizontalSizing, Sizing verticalSizing) {\n        this.horizontalSizing(horizontalSizing);\n        this.verticalSizing(verticalSizing);\n        return this;\n    }\n\n    /**\n     * Set the method this component uses to determine its size\n     * on both axes\n     *\n     * @param sizing The new sizing method to use on both axes\n     */\n    default UIComponent sizing(Sizing sizing) {\n        this.sizing(sizing, sizing);\n        return this;\n    }\n\n    /**\n     * Set the method this component uses to determine its size on the x-axis\n     */\n    UIComponent horizontalSizing(Sizing horizontalSizing);\n\n    /**\n     * @return The sizing method this component uses on the x-axis\n     */\n    @Contract(pure = true)\n    AnimatableProperty<Sizing> horizontalSizing();\n\n    /**\n     * Set the method this component uses to determine its size on the y-axis\n     */\n    UIComponent verticalSizing(Sizing verticalSizing);\n\n    /**\n     * @return The sizing method this component uses on the y-axis\n     */\n    @Contract(pure = true)\n    AnimatableProperty<Sizing> verticalSizing();\n\n    /**\n     * Set the id of this component. If this is not unique across the hierarchy,\n     * calls to {@link ParentUIComponent#childById(Class, String)} may not be deterministic\n     *\n     * @param id The new id of this component\n     */\n    UIComponent id(@Nullable String id);\n\n    /**\n     * @return The current id of this component\n     */\n    @Nullable String id();\n\n    /**\n     * Set the tooltip this component should display\n     * while hovered\n     *\n     * @param tooltip The tooltip to display\n     */\n    UIComponent tooltip(@Nullable List<ClientTooltipComponent> tooltip);\n\n    /**\n     * Set the tooltip of this component to the given\n     * text, without any wrapping applied\n     */\n    default UIComponent tooltip(@NotNull Collection<net.minecraft.network.chat.Component> tooltip) {\n        var components = new ArrayList<ClientTooltipComponent>();\n        for (var line : tooltip) components.add(ClientTooltipComponent.create(line.getVisualOrderText()));\n        this.tooltip(components);\n        return this;\n    }\n\n    /**\n     * Set the tooltip of this component to the given\n     * text, wrapping at newline characters\n     */\n    default UIComponent tooltip(@NotNull net.minecraft.network.chat.Component tooltip) {\n        var components = new ArrayList<ClientTooltipComponent>();\n        for (var line : Minecraft.getInstance().font.split(tooltip, Integer.MAX_VALUE)) {\n            components.add(ClientTooltipComponent.create(line));\n        }\n        this.tooltip(components);\n        return this;\n    }\n\n    /**\n     * @return The tooltip this component currently\n     * display while hovered\n     */\n    @Contract(pure = true)\n    @Nullable List<ClientTooltipComponent> tooltip();\n\n    /**\n     * Determine if this component should currently\n     * render its tooltip\n     *\n     * @param mouseX The mouse cursor's x-coordinate\n     * @param mouseY The mouse cursor's y-coordinate\n     * @return {@code true} if the tooltip should be rendered\n     */\n    default boolean shouldDrawTooltip(double mouseX, double mouseY) {\n        return this.tooltip() != null && !this.tooltip().isEmpty() && this.isInBoundingBox(mouseX, mouseY);\n    }\n\n    /**\n     * Inflate this component into some amount of available space\n     *\n     * @param space The available space for this component to expand into\n     */\n    void inflate(Size space);\n\n    /**\n     * Called when this component is mounted during the layout process,\n     * this must only ever happen after the component has been inflated\n     *\n     * @param parent The new parent of this component\n     * @param x      The new x position of this component\n     * @param y      The new y position of this component\n     */\n    void mount(ParentUIComponent parent, int x, int y);\n\n    /**\n     * Called when this component is being dismounted from its\n     * parent. This usually happens because the layout is being recalculated\n     * or the child has been removed - useful for releasing resources for example\n     * <p>\n     * <b>Note:</b> It is currently not guaranteed in any way that this method is\n     * invoked when the component tree becomes itself unreachable. You may still override\n     * this method to release resources if it becomes certain at an early point that\n     * they're not needed anymore, but generally resource management stays the responsibility\n     * of the individual component for the time being\n     *\n     * @param reason Why the component is being dismounted. If this is\n     *               {@link DismountReason#LAYOUT_INFLATION}, resources should still be held onto\n     *               as the component will be re-mounted right after\n     */\n    void dismount(DismountReason reason);\n\n    /**\n     * Execute the given closure immediately with this\n     * component as the argument. This is primarily useful for calling\n     * methods that don't return the component and could thus not be\n     * called inline when constructing the UI Tree.\n     * <p>\n     * All state updates emitted during execution of the closure are deferred\n     * and consolidated into a single update that's emitted after execution has\n     * finished. Thus, you can also employ this to efficiently update multiple\n     * properties on a component.\n     * <p>\n     * <b>It is imperative that the type parameter be declared to a type that\n     * this component can be represented as - otherwise an exception is thrown</b>\n     * <p>\n     * Example:\n     * <pre>\n     * container.child(Components.label(Text.of(\"Click\")).&lt;LabelComponent&gt;configure(label -> {\n     *     label.mouseDown().subscribe((mouseX, mouseY, button) -> {\n     *         System.out.println(\"Click\");\n     *         return true;\n     *     });\n     * }));\n     * </pre>\n     *\n     * @param closure The closure to execute\n     * @param <C>     A type this component can be represented as\n     * @return This component\n     */\n    <C extends UIComponent> C configure(Consumer<C> closure);\n\n    /**\n     * @return {@code true} if this component currently has a parent\n     */\n    @Contract(pure = true)\n    default boolean hasParent() {\n        return this.parent() != null;\n    }\n\n    /**\n     * @return The root component of this component's\n     * tree, or {@code null} if this component is not mounted\n     */\n    default ParentUIComponent root() {\n        var root = this.parent();\n        if (root == null) return null;\n\n        while (root.hasParent()) root = root.parent();\n        return root;\n    }\n\n    /**\n     * Remove this component from its parent, if\n     * it is currently mounted\n     */\n    default void remove() {\n        if (!this.hasParent()) return;\n        this.parent().queue(() -> {\n            this.parent().removeChild(this);\n        });\n    }\n\n    /**\n     * Called when the mouse has been clicked inside\n     * the bounding box of this component\n     *\n     * @param click\n     * @param doubled\n     * @return {@code true} if this component handled the click and no more\n     * components should be notified\n     */\n    boolean onMouseDown(MouseButtonEvent click, boolean doubled);\n\n    EventSource<MouseDown> mouseDown();\n\n    /**\n     * Called when a mouse button has been released\n     * while this component is focused\n     *\n     * @param click\n     * @return {@code true} if this component handled the event and no more\n     *                     components should be notified\n     */\n    boolean onMouseUp(MouseButtonEvent click);\n\n    EventSource<MouseUp> mouseUp();\n\n    /**\n     * Called when the mouse has been scrolled inside\n     * the bounding box of this component\n     *\n     * @param mouseX The x coordinate at which the mouse pointer is, relative\n     *               to this component's bounding box root\n     * @param mouseY The y coordinate at which the mouse pointer is, relative\n     *               to this component's bounding box root\n     * @param amount How far the mouse was scrolled\n     * @return {@code true} if this component handled the scroll event\n     * and no more components should be notified\n     */\n    boolean onMouseScroll(double mouseX, double mouseY, double amount);\n\n    EventSource<MouseScroll> mouseScroll();\n\n    /**\n     * Called when the mouse has been dragged\n     * while this component is focused\n     *\n     * @param click\n     * @param deltaX How far the mouse was moved on the x-axis\n     * @param deltaY How far the mouse was moved on the y-axis\n     * @return {@code true} if this component handled the mouse move and no more\n     * components should be notified\n     */\n    boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY);\n\n    EventSource<MouseDrag> mouseDrag();\n\n    /**\n     * Called when a key on the keyboard has been pressed\n     * while this component is focused\n     *\n     * @param input\n     * @return {@code true} if this component handled the key-press and no\n     *                     more components should be notified\n     */\n    boolean onKeyPress(KeyEvent input);\n\n    EventSource<KeyPress> keyPress();\n\n    /**\n     * Called when a keyboard input event occurred - namely when\n     * a key has been pressed and the OS determined it should result\n     * in a character being typed\n     *\n     * @param input\n     * @return {@code true} if this component handled the input and no\n     *                     * more components should be notified\n     */\n    boolean onCharTyped(CharacterEvent input);\n\n    EventSource<CharTyped> charTyped();\n\n    /**\n     * @return {@code true} if this component can gain focus\n     */\n    default boolean canFocus(FocusSource source) {\n        return false;\n    }\n\n    /**\n     * Called when this component gains focus, due\n     * to being clicked or selected via tab-cycling\n     */\n    void onFocusGained(FocusSource source);\n\n    EventSource<FocusGained> focusGained();\n\n    /**\n     * Called when this component loses focus\n     */\n    void onFocusLost();\n\n    EventSource<FocusLost> focusLost();\n\n    EventSource<MouseEnter> mouseEnter();\n\n    EventSource<MouseLeave> mouseLeave();\n\n    /**\n     * @return The style of cursor to use while the mouse is\n     * hovering this component\n     */\n    CursorStyle cursorStyle();\n\n    /**\n     * Set the style of cursor to use while the\n     * mouse is hovering this component\n     */\n    UIComponent cursorStyle(CursorStyle style);\n\n    /**\n     * Update the state of this component\n     * before drawing the next frame\n     *\n     * @param delta  The duration of the last frame, in partial ticks\n     * @param mouseX The mouse pointer's x-coordinate\n     * @param mouseY The mouse pointer's y-coordinate\n     */\n    default void update(float delta, int mouseX, int mouseY) {\n        this.margins().update(delta);\n        this.positioning().update(delta);\n        this.horizontalSizing().update(delta);\n        this.verticalSizing().update(delta);\n    }\n\n    /**\n     * Test whether the given coordinates\n     * are inside this component's bounding box\n     *\n     * @param x The x-coordinate to test\n     * @param y The y-coordinate to test\n     * @return {@code true} if this component's bounding box encloses\n     * the given coordinates\n     */\n    @Override\n    default boolean isInBoundingBox(double x, double y) {\n        return PositionedRectangle.super.isInBoundingBox(x, y);\n    }\n\n    /**\n     * @return The current size of this component's content + its margins\n     */\n    default Size fullSize() {\n        var margins = this.margins().get();\n        return Size.of(this.width() + margins.horizontal(), this.height() + margins.vertical());\n    }\n\n    /**\n     * Read the properties, and potentially children, of this\n     * component from the given XML element\n     *\n     * @param model    The UI model that's being instantiated,\n     *                 used for creating child components\n     * @param element  The XML element representing this component\n     * @param children The child elements of the XML element representing\n     *                 this component by tag name, without duplicates\n     */\n    default void parseProperties(UIModel model, Element element, Map<String, Element> children) {\n        if (!element.getAttribute(\"id\").isBlank()) {\n            this.id(element.getAttribute(\"id\").strip());\n        }\n\n        UIParsing.apply(children, \"margins\", Insets::parse, this::margins);\n        UIParsing.apply(children, \"positioning\", Positioning::parse, this::positioning);\n        UIParsing.apply(children, \"cursor-style\", UIParsing.parseEnum(CursorStyle.class), this::cursorStyle);\n        UIParsing.apply(children, \"tooltip-text\", UIParsing::parseText, this::tooltip);\n\n        if (children.containsKey(\"sizing\")) {\n            var sizingValues = UIParsing.childElements(children.get(\"sizing\"));\n            UIParsing.apply(sizingValues, \"vertical\", Sizing::parse, this::verticalSizing);\n            UIParsing.apply(sizingValues, \"horizontal\", Sizing::parse, this::horizontalSizing);\n        }\n    }\n\n    /**\n     * @return The current width of the bounding box\n     * of this component\n     */\n    @Override\n    @Contract(pure = true)\n    int width();\n\n    /**\n     * @return The current height of the bounding box\n     * of this component\n     */\n    @Override\n    @Contract(pure = true)\n    int height();\n\n    /**\n     * @return The current x-coordinate of the top-left\n     * corner of the bounding box of this component\n     * <p>\n     * As a general rule of thumb, this property should be used\n     * whenever the component's position is queried during rendering,\n     * input processing and s on. If however, the position is required\n     * in the context of a layout operation, {@link #baseX()} is almost\n     * always the correct choice instead\n     */\n    @Override\n    @Contract(pure = true)\n    int x();\n\n    /**\n     * @return The current x-coordinate of this component's\n     * <i>base point</i> - the point on which it bases\n     * layout calculations.\n     * <p>\n     * For the majority of components this will be identical\n     * to {@link #x()} as they don't have special logic. A notable\n     * exception is the {@link io.wispforest.owo.ui.container.DraggableContainer}\n     * which internally applies a separate offset from dragging\n     */\n    default int baseX() {\n        return this.x();\n    }\n\n    /**\n     * Set the x-coordinate of the top-left corner of the\n     * bounding box of this component.\n     * <p>\n     * This method will usually only be called by the\n     * parent component - users of the API\n     * should instead alter properties to this component\n     * to ensure proper layout updates\n     *\n     * @param x The new x-coordinate of the top-left corner of the\n     *          bounding box of this component\n     * @see #positioning(Positioning)\n     * @see #margins(Insets)\n     */\n    void updateX(int x);\n\n    /**\n     * @return The current y-coordinate of the top-left\n     * corner of the bounding box of this component\n     * <p>\n     * As a general rule of thumb, this property should be used\n     * whenever the component's position is queried during rendering,\n     * input processing and s on. If however, the position is required\n     * in the context of a layout operation, {@link #baseY()} is almost\n     * always the correct choice instead\n     */\n    @Override\n    @Contract(pure = true)\n    int y();\n\n    /**\n     * @return The current y-coordinate of this component's\n     * <i>base point</i> - the point on which it bases\n     * layout calculations.\n     * <p>\n     * For the majority of components this will be identical\n     * to {@link #y()} as they don't have special logic. A notable\n     * exception is the {@link io.wispforest.owo.ui.container.DraggableContainer}\n     * which internally applies a separate offset from dragging\n     */\n    default int baseY() {\n        return this.y();\n    }\n\n    /**\n     * Set the y-coordinate of the top-left corner of the\n     * bounding box of this component.\n     * <p>\n     * This method will usually only be called by the\n     * parent component - users of the API\n     * should instead alter properties to this component\n     * to ensure proper layout updates\n     *\n     * @param y The new y-coordinate of the top-left corner of the\n     *          bounding box of this component\n     * @see #positioning(Positioning)\n     * @see #margins(Insets)\n     */\n    void updateY(int y);\n\n    /**\n     * Set the coordinates of the top-left corner of the\n     * bounding box of this component.\n     * <p>\n     * This method will usually only be called by the\n     * parent component - users of the API\n     * should instead alter properties to this component\n     * to ensure proper layout updates\n     *\n     * @param y The new coordinates of the top-left corner of the\n     *          bounding box of this component\n     * @see #positioning(Positioning)\n     * @see #margins(Insets)\n     */\n    default void moveTo(int x, int y) {\n        this.updateX(x);\n        this.updateY(y);\n    }\n\n    /**\n     * @return a textual representation of the component's details for use in debugging with the inspector HUD.\n     *          Default implementation contains positioning, size and margins.\n     * @see OwoUIGraphics#drawInspector(ParentUIComponent, double, double, boolean)\n     */\n    default MutableComponent inspectorDescriptor() {\n        final var margins = this.margins().get();\n        return Component.literal(this.x() + \",\" + this.y() + \" (\" + this.width() + \",\" + this.height() + \")\")\n                .append(\n                        Component.literal(\" <\" + margins.top() + \",\" + margins.bottom() + \",\" + margins.left() + \",\" + margins.right() + \">\")\n                                .setStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW))\n                );\n    }\n\n    enum FocusSource {\n        /**\n         * The component has been clicked\n         */\n        MOUSE_CLICK,\n\n        /**\n         * The component has been selected by\n         * cycling focus via the keyboard\n         */\n        KEYBOARD_CYCLE\n    }\n\n    enum DismountReason {\n        /**\n         * The child has been dismounted because the parent's layout\n         * is being inflated\n         */\n        LAYOUT_INFLATION,\n        /**\n         * The child has been dismounted because it has been removed\n         * from its parent\n         */\n        REMOVED\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/core/VerticalAlignment.java",
    "content": "package io.wispforest.owo.ui.core;\n\nimport org.w3c.dom.Element;\n\nimport java.util.Locale;\n\npublic enum VerticalAlignment {\n    TOP, CENTER, BOTTOM;\n\n    public int align(int componentWidth, int span) {\n        return switch (this) {\n            case TOP -> 0;\n            case CENTER -> span / 2 - componentWidth / 2;\n            case BOTTOM -> span - componentWidth;\n        };\n    }\n\n    public static VerticalAlignment parse(Element element) {\n        return valueOf(element.getTextContent().strip().toUpperCase(Locale.ROOT));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/CharTyped.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.input.CharacterEvent;\n\npublic interface CharTyped {\n    boolean onCharTyped(CharacterEvent input);\n\n    static EventStream<CharTyped> newStream() {\n        return new EventStream<>(subscribers -> (input) -> {\n            var anyTriggered = false;\n            for (var subscriber : subscribers) {\n                anyTriggered |= subscriber.onCharTyped(input);\n            }\n            return anyTriggered;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/ClientRenderCallback.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport net.fabricmc.fabric.api.event.Event;\nimport net.fabricmc.fabric.api.event.EventFactory;\nimport net.minecraft.client.Minecraft;\n\npublic interface ClientRenderCallback {\n\n    /**\n     * Invoked just before the client's window enters the 'Render' phase, after the client\n     * has ticked and cleared the render task queue\n     */\n    Event<ClientRenderCallback> BEFORE = EventFactory.createArrayBacked(ClientRenderCallback.class, callbacks -> (client) -> {\n        for (var callback : callbacks) {\n            callback.onRender(client);\n        }\n    });\n\n    Event<ClientRenderCallback> BEFORE_SWAP = EventFactory.createArrayBacked(ClientRenderCallback.class, callbacks -> (client) -> {\n        for (var callback : callbacks) {\n            callback.onRender(client);\n        }\n    });\n\n    /**\n     * Called just after the client has finished rendering and drawing the\n     * current frame and swapped buffers\n     */\n    Event<ClientRenderCallback> AFTER = EventFactory.createArrayBacked(ClientRenderCallback.class, callbacks -> (client) -> {\n        for (var callback : callbacks) {\n            callback.onRender(client);\n        }\n    });\n\n    void onRender(Minecraft client);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/FocusGained.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.util.EventStream;\n\npublic interface FocusGained {\n    void onFocusGained(UIComponent.FocusSource source);\n\n    static EventStream<FocusGained> newStream() {\n        return new EventStream<>(subscribers -> source -> {\n            for (var subscriber : subscribers) {\n                subscriber.onFocusGained(source);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/FocusLost.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\n\npublic interface FocusLost {\n    void onFocusLost();\n\n    static EventStream<FocusLost> newStream() {\n        return new EventStream<>(subscribers -> () -> {\n            for (var subscriber : subscribers) {\n                subscriber.onFocusLost();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/KeyPress.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.input.KeyEvent;\n\npublic interface KeyPress {\n    boolean onKeyPress(KeyEvent input);\n\n    static EventStream<KeyPress> newStream() {\n        return new EventStream<>(subscribers -> (input) -> {\n            var anyTriggered = false;\n            for (var subscriber : subscribers) {\n                anyTriggered |= subscriber.onKeyPress(input);\n            }\n            return anyTriggered;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/MouseDown.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.input.MouseButtonEvent;\n\npublic interface MouseDown {\n    boolean onMouseDown(MouseButtonEvent click, boolean doubled);\n\n    static EventStream<MouseDown> newStream() {\n        return new EventStream<>(subscribers -> (click, doubled) -> {\n            var anyTriggered = false;\n            for (var subscriber : subscribers) {\n                anyTriggered |= subscriber.onMouseDown(click, doubled);\n            }\n            return anyTriggered;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/MouseDrag.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.input.MouseButtonEvent;\n\npublic interface MouseDrag {\n    boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY);\n\n    static EventStream<MouseDrag> newStream() {\n        return new EventStream<>(subscribers -> (click, deltaX, deltaY) -> {\n            var anyTriggered = false;\n            for (var subscriber : subscribers) {\n                anyTriggered |= subscriber.onMouseDrag(click, deltaX, deltaY);\n            }\n            return anyTriggered;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/MouseEnter.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\n\npublic interface MouseEnter {\n    void onMouseEnter();\n\n    static EventStream<MouseEnter> newStream() {\n        return new EventStream<>(subscribers -> () -> {\n            for (var subscriber : subscribers) {\n                subscriber.onMouseEnter();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/MouseLeave.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\n\npublic interface MouseLeave {\n    void onMouseLeave();\n\n    static EventStream<MouseLeave> newStream() {\n        return new EventStream<>(subscribers -> () -> {\n            for (var subscriber : subscribers) {\n                subscriber.onMouseLeave();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/MouseScroll.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\n\npublic interface MouseScroll {\n    boolean onMouseScroll(double mouseX, double mouseY, double amount);\n\n    static EventStream<MouseScroll> newStream() {\n        return new EventStream<>(subscribers -> (mouseX, mouseY, amount) -> {\n            var anyTriggered = false;\n            for (var subscriber : subscribers) {\n                anyTriggered |= subscriber.onMouseScroll(mouseX, mouseY, amount);\n            }\n            return anyTriggered;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/MouseUp.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport io.wispforest.owo.util.EventStream;\nimport net.minecraft.client.input.MouseButtonEvent;\n\npublic interface MouseUp {\n    boolean onMouseUp(MouseButtonEvent click);\n\n    static EventStream<MouseUp> newStream() {\n        return new EventStream<>(subscribers -> (click) -> {\n            var anyTriggered = false;\n            for (var subscriber : subscribers) {\n                anyTriggered |= subscriber.onMouseUp(click);\n            }\n            return anyTriggered;\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/event/WindowResizeCallback.java",
    "content": "package io.wispforest.owo.ui.event;\n\nimport com.mojang.blaze3d.platform.Window;\nimport net.fabricmc.fabric.api.event.Event;\nimport net.fabricmc.fabric.api.event.EventFactory;\nimport net.minecraft.client.Minecraft;\n\npublic interface WindowResizeCallback {\n\n    Event<WindowResizeCallback> EVENT = EventFactory.createArrayBacked(WindowResizeCallback.class, callbacks -> (client, window) -> {\n        for (var callback : callbacks) {\n            callback.onResized(client, window);\n        }\n    });\n\n    /**\n     * Called after the client's window has been resized\n     *\n     * @param client The currently active client\n     * @param window The window which was resized\n     */\n    void onResized(Minecraft client, Window window);\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/hud/Hud.java",
    "content": "package io.wispforest.owo.ui.hud;\n\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.OwoUIAdapter;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport io.wispforest.owo.ui.event.ClientRenderCallback;\nimport io.wispforest.owo.ui.event.WindowResizeCallback;\nimport net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;\nimport net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\n/**\n * A utility for displaying owo-ui components on the\n * in-game HUD - rendered during {@link HudRenderCallback}\n */\npublic class Hud {\n\n    static @Nullable OwoUIAdapter<FlowLayout> adapter = null;\n    static boolean suppress = false;\n\n    private static final Map<Identifier, UIComponent> activeComponents = new HashMap<>();\n    private static final List<Consumer<FlowLayout>> pendingActions = new ArrayList<>();\n\n    /**\n     * Add a new component to be rendered on the in-game HUD.\n     * The root container used by the HUD does not support layout\n     * positioning - the component supplied by {@code component}\n     * must be explicitly positioned via either {@link io.wispforest.owo.ui.core.Positioning#absolute(int, int)}\n     * or {@link io.wispforest.owo.ui.core.Positioning#relative(int, int)}\n     *\n     * @param id        An ID uniquely describing this HUD component\n     * @param component A function creating the component\n     *                  when the HUD is first rendered\n     */\n    public static void add(Identifier id, Supplier<UIComponent> component) {\n        pendingActions.add(flowLayout -> {\n            var instance = component.get();\n\n            flowLayout.child(instance);\n            activeComponents.put(id, instance);\n        });\n    }\n\n    /**\n     * Remove the HUD component described by the given ID\n     *\n     * @param id The ID of the HUD component to remove\n     */\n    public static void remove(Identifier id) {\n        pendingActions.add(flowLayout -> {\n            var component = activeComponents.get(id);\n            if (component == null) return;\n\n            flowLayout.removeChild(component);\n            activeComponents.remove(id);\n        });\n    }\n\n    /**\n     * Get the HUD component described by the given ID\n     *\n     * @param id The ID of the HUD component to query\n     * @return The relevant HUD component, or {@code null} if there is none\n     */\n    public static @Nullable UIComponent getComponent(Identifier id) {\n        return activeComponents.get(id);\n    }\n\n    /**\n     * @return {@code true} if there is an active HUD component described by {@code id}\n     */\n    public static boolean hasComponent(Identifier id) {\n        return activeComponents.containsKey(id);\n    }\n\n    private static void initializeAdapter() {\n        var window = Minecraft.getInstance().getWindow();\n        adapter = OwoUIAdapter.createWithoutScreen(\n            0, 0, window.getGuiScaledWidth(), window.getGuiScaledHeight(), HudContainer::new\n        );\n\n        adapter.inflateAndMount();\n    }\n\n    static {\n        WindowResizeCallback.EVENT.register((client, window) -> {\n            if (adapter == null) return;\n            adapter.moveAndResize(0, 0, window.getGuiScaledWidth(), window.getGuiScaledHeight());\n        });\n\n        ClientRenderCallback.BEFORE.register(client -> {\n            if (client.level == null) return;\n            if (!pendingActions.isEmpty()) {\n                if (adapter == null) initializeAdapter();\n\n                pendingActions.forEach(action -> action.accept(adapter.rootComponent));\n                pendingActions.clear();\n            }\n        });\n\n        HudElementRegistry.addLast(Identifier.fromNamespaceAndPath(\"owo\", \"owo_ui_hud\"), (context, tickCounter) -> {\n            if (adapter == null || suppress || Minecraft.getInstance().options.hideGui) return;\n            adapter.render(context, -69, -69, tickCounter.getGameTimeDeltaPartialTick(false));\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/hud/HudContainer.java",
    "content": "package io.wispforest.owo.ui.hud;\n\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.Positioning;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.Consumer;\n\n/**\n * Very simple extension of {@link io.wispforest.owo.ui.container.FlowLayout} that\n * does not allow children to be layout-positioned, used by {@link Hud}\n */\npublic class HudContainer extends FlowLayout {\n\n    protected HudContainer(Sizing horizontalSizing, Sizing verticalSizing) {\n        super(horizontalSizing, verticalSizing, Algorithm.VERTICAL);\n    }\n\n    @Override\n    protected void mountChild(@Nullable UIComponent child, Consumer<UIComponent> layoutFunc) {\n        if (child == null) return;\n\n        if (child.positioning().get().type == Positioning.Type.LAYOUT) {\n            throw new IllegalStateException(\"owo-ui HUD components must be explicitly positioned\");\n        } else {\n            super.mountChild(child, layoutFunc);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/hud/HudInspectorScreen.java",
    "content": "package io.wispforest.owo.ui.hud;\n\nimport io.wispforest.owo.ui.util.CommandOpenedScreen;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\n\npublic class HudInspectorScreen extends Screen implements CommandOpenedScreen {\n\n    public HudInspectorScreen() {\n        super(Component.empty());\n        if (Hud.adapter != null) {\n            Hud.suppress = true;\n            Hud.adapter.enableInspector = true;\n        }\n    }\n\n    @Override\n    public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {\n        super.render(graphics, mouseX, mouseY, delta);\n\n        if (Hud.adapter == null) return;\n        Hud.adapter.render(graphics, mouseX, mouseY, delta);\n    }\n\n    @Override\n    public void removed() {\n        if (Hud.adapter != null) {\n            Hud.suppress = false;\n            Hud.adapter.enableInspector = false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/inject/GreedyInputUIComponent.java",
    "content": "package io.wispforest.owo.ui.inject;\n\nimport io.wispforest.owo.ui.core.UIComponent;\n\n/**\n * A marker interface for components which consume\n * text input when focused - this is used to prevent handled\n * screens from closing when said component is focused and the\n * inventory key is pressed\n */\npublic interface GreedyInputUIComponent extends UIComponent {}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/inject/UIComponentStub.java",
    "content": "package io.wispforest.owo.ui.inject;\n\nimport io.wispforest.owo.ui.component.VanillaWidgetComponent;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.event.*;\nimport io.wispforest.owo.ui.util.FocusHandler;\nimport io.wispforest.owo.util.EventSource;\nimport net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;\nimport net.minecraft.client.input.CharacterEvent;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\nimport java.util.function.Consumer;\n\n/**\n * Stub-version of component which adds implementations for all methods\n * that unconditionally throw - used for interface-injecting onto\n * vanilla widgets\n */\npublic interface UIComponentStub extends UIComponent {\n\n    @Override\n    default void draw(OwoUIGraphics graphics, int mouseX, int mouseY, float partialTicks, float delta) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default @Nullable ParentUIComponent parent() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default @Nullable FocusHandler focusHandler() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default UIComponent positioning(Positioning positioning) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default AnimatableProperty<Positioning> positioning() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default UIComponent margins(Insets margins) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default AnimatableProperty<Insets> margins() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default UIComponent horizontalSizing(Sizing horizontalSizing) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default UIComponent verticalSizing(Sizing verticalSizing) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default AnimatableProperty<Sizing> horizontalSizing() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default AnimatableProperty<Sizing> verticalSizing() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<MouseEnter> mouseEnter() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<MouseLeave> mouseLeave() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default CursorStyle cursorStyle() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default UIComponent cursorStyle(CursorStyle style) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default UIComponent tooltip(List<ClientTooltipComponent> tooltip) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default List<ClientTooltipComponent> tooltip() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default void inflate(Size space) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default void mount(ParentUIComponent parent, int x, int y) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default void dismount(DismountReason reason) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default <C extends UIComponent> C configure(Consumer<C> closure) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default int width() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default int height() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default boolean onMouseDown(MouseButtonEvent click, boolean doubled) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<MouseDown> mouseDown() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default boolean onMouseUp(MouseButtonEvent click) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<MouseUp> mouseUp() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default boolean onMouseScroll(double mouseX, double mouseY, double amount) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<MouseScroll> mouseScroll() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default boolean onMouseDrag(MouseButtonEvent click, double deltaX, double deltaY) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<MouseDrag> mouseDrag() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default boolean onKeyPress(KeyEvent input) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<KeyPress> keyPress() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default boolean onCharTyped(CharacterEvent input) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<CharTyped> charTyped() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default void onFocusGained(FocusSource source) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<FocusGained> focusGained() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default void onFocusLost() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default EventSource<FocusLost> focusLost() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default int x() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default void updateX(int x) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default int y() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default void updateY(int y) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default UIComponent id(@Nullable String id) {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    @Override\n    default @Nullable String id() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    default VanillaWidgetComponent widgetWrapper() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    default int xOffset() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    default int yOffset() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    default int widthOffset() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n\n    default int heightOffset() {\n        throw new IllegalStateException(\"Interface stub method called\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/layers/Layer.java",
    "content": "package io.wispforest.owo.ui.layers;\n\nimport io.wispforest.owo.mixin.ui.layers.AbstractContainerScreenAccessor;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.util.pond.OwoScreenExtension;\nimport net.minecraft.client.gui.components.AbstractWidget;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.gui.layouts.Layout;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.BiFunction;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\n\npublic class Layer<S extends Screen, R extends ParentUIComponent> {\n\n    protected final BiFunction<Sizing, Sizing, R> rootComponentMaker;\n    protected final Consumer<Layer<S, R>.Instance> instanceInitializer;\n\n    protected Layer(BiFunction<Sizing, Sizing, R> rootComponentMaker, Consumer<Layer<S, R>.Instance> instanceInitializer) {\n        this.rootComponentMaker = rootComponentMaker;\n        this.instanceInitializer = instanceInitializer;\n    }\n\n    public Instance instantiate(S screen) {\n        return new Instance(screen);\n    }\n\n    public Instance getInstance(S screen) {\n        return ((OwoScreenExtension) screen).owo$getInstance(this);\n    }\n\n    public class Instance {\n\n        /**\n         * The screen this instance is attached to\n         */\n        public final S screen;\n\n        /**\n         * The UI adapter of this instance - get the {@link OwoUIAdapter#rootComponent}\n         * from this to start building your UI tree\n         */\n        public final OwoUIAdapter<R> adapter;\n\n        /**\n         * Whether this layer should aggressively update widget-relative\n         * positioning every frame - useful if the targeted widget moves frequently\n         */\n        public boolean aggressivePositioning = false;\n\n        protected final List<Runnable> layoutUpdaters = new ArrayList<>();\n\n        protected Instance(S screen) {\n            this.screen = screen;\n            this.adapter = OwoUIAdapter.createWithoutScreen(0, 0, screen.width, screen.height, Layer.this.rootComponentMaker);\n            Layer.this.instanceInitializer.accept(this);\n        }\n\n        @ApiStatus.Internal\n        public void resize(int width, int height) {\n            this.adapter.moveAndResize(0, 0, width, height);\n        }\n\n        /**\n         * Find a widget in the attached screen's widget tree\n         *\n         * @param locator A predicate to match which identifies the targeted widget\n         * @return The targeted widget, or {@link null} if the predicate was never matched\n         */\n        public @Nullable AbstractWidget queryWidget(Predicate<AbstractWidget> locator) {\n            var widgets = new ArrayList<AbstractWidget>();\n            for (var element : this.screen.children()) collectChildren(element, widgets);\n\n            AbstractWidget widget = null;\n            for (var candidate : widgets) {\n                if (!locator.test(candidate)) continue;\n                widget = candidate;\n                break;\n            }\n\n            return widget;\n        }\n\n        /**\n         * Align the given component to a widget in the attached screen's\n         * widget tree. The widget is located by passing the locator predicate to\n         * {@link #queryWidget(Predicate)} and getting the position of the resulted widget.\n         * <p>\n         * If no widget can be found, the component gets positioned at 0,0\n         *\n         * @param locator       A predicate to match which identifies the targeted widget\n         * @param anchor        On which side of the targeted widget to anchor the component\n         * @param justification How far along the anchor side of the widget in positive axis direction\n         *                      to position the component\n         * @param component     The component to position\n         */\n        public void alignComponentToWidget(Predicate<AbstractWidget> locator, AnchorSide anchor, float justification, UIComponent component) {\n            this.layoutUpdaters.add(() -> {\n                var widget = this.queryWidget(locator);\n\n                if (widget == null) {\n                    component.positioning(Positioning.absolute(0, 0));\n                    return;\n                }\n\n                var size = component.fullSize();\n                switch (anchor) {\n                    case TOP -> component.positioning(Positioning.absolute(\n                            (int) (widget.getX() + (widget.getWidth() - size.width()) * justification),\n                            widget.getY() - size.height()\n                    ));\n                    case RIGHT -> component.positioning(Positioning.absolute(\n                            widget.getX() + widget.getWidth(),\n                            (int) (widget.getY() + (widget.getHeight() - size.height()) * justification)\n                    ));\n                    case BOTTOM -> component.positioning(Positioning.absolute(\n                            (int) (widget.getX() + (widget.getWidth() - size.width()) * justification),\n                            widget.getY() + widget.getHeight()\n                    ));\n                    case LEFT -> component.positioning(Positioning.absolute(\n                            widget.getX() - size.width(),\n                            (int) (widget.getY() + (widget.getHeight() - size.height()) * justification)\n                    ));\n                }\n            });\n        }\n\n        /**\n         * Align the given component relative to the handled screen coordinates\n         * as used by vanilla for positioning slots\n         * <p>\n         * For obvious reasons, this method may only be invoked on layers which are\n         * pushed onto instances of {@link AbstractContainerScreen}\n         *\n         * @param component The component to position\n         * @param x         The X coordinate of the component, relative to the handled screen's origin\n         * @param y         The Y coordinate of the component, relative to the handled screen's origin\n         */\n        public void alignComponentToHandledScreenCoordinates(UIComponent component, int x, int y) {\n            if (!(this.screen instanceof AbstractContainerScreen<?> handledScreen)) {\n                throw new IllegalStateException(\"Handled screen coordinates only exist on screens which extend HandledScreen<?>\");\n            }\n\n            this.layoutUpdaters.add(() -> {\n                component.positioning(Positioning.absolute(\n                        ((AbstractContainerScreenAccessor) handledScreen).owo$getRootX() + x,\n                        ((AbstractContainerScreenAccessor) handledScreen).owo$getRootY() + y\n                ));\n            });\n        }\n\n        @ApiStatus.Internal\n        public void dispatchLayoutUpdates() {\n            this.layoutUpdaters.forEach(Runnable::run);\n        }\n\n        private static void collectChildren(GuiEventListener element, List<AbstractWidget> children) {\n            if (element instanceof AbstractWidget widget) children.add(widget);\n            if (element instanceof Layout layout) {\n                layout.visitWidgets(child -> collectChildren(child, children));\n            }\n        }\n\n        public enum AnchorSide {\n            TOP, BOTTOM, LEFT, RIGHT\n        }\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/layers/Layers.java",
    "content": "package io.wispforest.owo.ui.layers;\n\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.Multimap;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.util.pond.OwoScreenExtension;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents;\nimport net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents;\nimport net.fabricmc.fabric.api.event.Event;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.resources.Identifier;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.function.BiFunction;\nimport java.util.function.Consumer;\n\n/**\n * A system for adding owo-ui components onto existing screens.\n * <p>\n * You can create a new layer by calling {@link #add(BiFunction, Consumer, Class[])}. The\n * second argument to this function is the instance initializer, which is where you configure\n * instances of your layer added onto screens when they get initialized. This is the place to\n * configure the UI adapter of your layer as well as building your UI tree onto the root\n * component of said adapter\n * <p>\n * Just like proper owo-ui screens, layers preserve state when the client's window\n * is resized - they are only initialized once, when the screen is first opened\n */\npublic final class Layers {\n\n    /**\n     * The event phase during which owo-ui layer instances are created and\n     * initialized. This runs after the default phase\n     */\n    public static final Identifier INIT_PHASE = Owo.id(\"init-layers\");\n\n    private static final Multimap<Class<? extends Screen>, Layer<?, ?>> LAYERS = HashMultimap.create();\n\n    /**\n     * Add a new layer to the given screens\n     *\n     * @param rootComponentMaker  A function which will create the root component of this layer\n     * @param instanceInitializer A function which will initialize any instances of this layer which get created.\n     *                            This is where you add components or configure the UI adapter of the generated layer instance\n     * @param screenClasses       The screens onto which to add the new layer\n     */\n    @SafeVarargs\n    public static <S extends Screen, R extends ParentUIComponent> Layer<S, R> add(BiFunction<Sizing, Sizing, R> rootComponentMaker, Consumer<Layer<S, R>.Instance> instanceInitializer, Class<? extends S>... screenClasses) {\n        final var layer = new Layer<>(rootComponentMaker, instanceInitializer);\n        for (var screenClass : screenClasses) {\n            LAYERS.put(screenClass, layer);\n        }\n        return layer;\n    }\n\n    /**\n     * Get all layers associated with a given screen\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <S extends Screen> Collection<Layer<S, ?>> getLayers(Class<S> screenClass) {\n        return (Collection<Layer<S, ?>>) (Object) LAYERS.get(screenClass);\n    }\n\n    /**\n     * Get all layer instances currently present on the given screen\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <S extends Screen> List<Layer<S, ?>.Instance> getInstances(S screen) {\n        return (List<Layer<S, ?>.Instance>) (Object) ((OwoScreenExtension) screen).owo$getInstancesView();\n    }\n\n    static {\n        ScreenEvents.AFTER_INIT.addPhaseOrdering(Event.DEFAULT_PHASE, INIT_PHASE);\n        ScreenEvents.AFTER_INIT.register(INIT_PHASE, (client, screeen, scaledWidth, scaledHeight) -> {\n            ((OwoScreenExtension) screeen).owo$updateLayers();\n\n            ScreenEvents.remove(screeen).register(screen -> {\n                for (var instance : getInstances(screen)) {\n                    instance.adapter.dispose();\n                }\n            });\n\n            ScreenEvents.beforeRender(screeen).register((screen, context, mouseX, mouseY, tickDelta) -> {\n                for (var instance : getInstances(screen)) {\n                    if (instance.aggressivePositioning) instance.dispatchLayoutUpdates();\n                }\n            });\n\n            ScreenEvents.afterRender(screeen).register((screen, context, mouseX, mouseY, tickDelta) -> {\n//                context.draw();\n                for (var instance : getInstances(screen)) {\n                    instance.adapter.render(context, mouseX, mouseY, tickDelta);\n                }\n\n                for (var instance : getInstances(screen)) {\n                    instance.adapter.drawTooltip(context, mouseX, mouseY, tickDelta);\n                }\n            });\n\n            ScreenMouseEvents.allowMouseClick(screeen).register((screen, click) -> {\n                boolean handled;\n                for (var instance : getInstances(screen)) {\n                    handled = instance.adapter.mouseClicked(click, false);\n                    if (handled) return false;\n                }\n\n                return true;\n            });\n\n            ScreenMouseEvents.allowMouseRelease(screeen).register((screen, click) -> {\n                boolean handled;\n                for (var instance : getInstances(screen)) {\n                    handled = instance.adapter.mouseReleased(click);\n                    if (handled) return false;\n                }\n\n                return true;\n            });\n\n            ScreenMouseEvents.allowMouseScroll(screeen).register((screen, mouseX, mouseY, horizontalAmount, verticalAmount) -> {\n                boolean handled;\n                for (var instance : getInstances(screen)) {\n                    handled = instance.adapter.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);\n                    if (handled) return false;\n                }\n\n                return true;\n            });\n\n            ScreenKeyboardEvents.allowKeyPress(screeen).register((screen, keyInput) -> {\n                boolean handled;\n                for (var instance : getInstances(screen)) {\n                    handled = instance.adapter.keyPressed(keyInput);\n                    if (handled) return false;\n                }\n\n                return true;\n            });\n\n            ScreenKeyboardEvents.allowKeyRelease(screeen).register((screen, keyInput) -> {\n                boolean handled;\n                for (var instance : getInstances(screen)) {\n                    handled = instance.adapter.keyReleased(keyInput);\n                    if (handled) return false;\n                }\n\n                return true;\n            });\n        });\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/parsing/ConfigureHotReloadScreen.java",
    "content": "package io.wispforest.owo.ui.parsing;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.LabelComponent;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.util.CommandOpenedScreen;\nimport io.wispforest.owo.ui.util.UISounds;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.util.Util;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.util.tinyfd.TinyFileDialogs;\n\nimport java.nio.file.Path;\nimport java.util.concurrent.CompletableFuture;\n\npublic class ConfigureHotReloadScreen extends BaseUIModelScreen<FlowLayout> implements CommandOpenedScreen {\n\n    private final @Nullable Screen parent;\n\n    private final Identifier modelId;\n    private @Nullable Path reloadLocation;\n\n    private LabelComponent fileNameLabel;\n\n    public ConfigureHotReloadScreen(Identifier modelId, @Nullable Screen parent) {\n        super(FlowLayout.class, DataSource.asset(Owo.id(\"configure_hot_reload\")));\n        this.parent = parent;\n\n        this.modelId = modelId;\n        this.reloadLocation = UIModelLoader.getHotReloadPath(this.modelId);\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        rootComponent.childById(LabelComponent.class, \"ui-model-label\").text(Component.translatable(\"text.owo.configure_hot_reload.model\", this.modelId));\n        this.fileNameLabel = rootComponent.childById(LabelComponent.class, \"file-name-label\");\n        this.updateFileNameLabel();\n\n        rootComponent.childById(ButtonComponent.class, \"choose-button\").onPress(button -> {\n            CompletableFuture.runAsync(() -> {\n                var newPath = TinyFileDialogs.tinyfd_openFileDialog(\"Choose UI Model source\", null, null, null, false);\n                if (newPath != null) this.reloadLocation = Path.of(newPath);\n            }, Util.backgroundExecutor()).whenComplete((unused, throwable) -> {\n                this.updateFileNameLabel();\n            });\n        });\n\n        rootComponent.childById(ButtonComponent.class, \"save-button\").onPress(button -> {\n            UIModelLoader.setHotReloadPath(this.modelId, this.reloadLocation);\n            this.onClose();\n        });\n\n        rootComponent.childById(LabelComponent.class, \"close-label\").mouseDown().subscribe((click, doubled) -> {\n            UISounds.playInteractionSound();\n            this.onClose();\n            return true;\n        });\n    }\n\n    @Override\n    public void onClose() {\n        this.minecraft.setScreen(this.parent);\n    }\n\n    private void updateFileNameLabel() {\n        this.fileNameLabel.text(Component.translatable(\n                \"text.owo.configure_hot_reload.reload_from\",\n                this.reloadLocation == null ? Component.translatable(\"text.owo.configure_hot_reload.reload_from.unset\") : this.reloadLocation\n        ));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/parsing/IncompatibleUIModelException.java",
    "content": "package io.wispforest.owo.ui.parsing;\n\n/**\n * Describes an error that occurred because the UIModel provided\n * to a method did not match the expectations set by said method.\n * These expectations are most often expressed in terms of component\n * classes, a violation of which will throw this exception\n */\npublic class IncompatibleUIModelException extends RuntimeException {\n\n    public IncompatibleUIModelException(String message) {\n        super(message);\n    }\n\n    public IncompatibleUIModelException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/parsing/UIModel.java",
    "content": "package io.wispforest.owo.ui.parsing;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.core.OwoUIAdapter;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Attr;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.Text;\nimport org.xml.sax.SAXException;\n\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayDeque;\nimport java.util.Deque;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.regex.MatchResult;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * A model of a UI hierarchy parsed from an\n * XML definition. You can use this to create a UI adapter for your\n * screen with {@link #createAdapter(Class, Screen)} as well as expanding\n * templates via {@link #expandTemplate(Class, String, Map)}\n */\npublic class UIModel {\n\n    private static final Pattern PARAMETER_PATTERN = Pattern.compile(\"\\\\{\\\\{[-_a-zA-Z]+}}\");\n    private static final DocumentBuilder DOCUMENT_BUILDER;\n\n    static {\n        try {\n            DOCUMENT_BUILDER = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder();\n        } catch (ParserConfigurationException e) {\n            throw new RuntimeException(\"we love checked exceptions, we love checked exceptions, we love checked exceptions\", e);\n        }\n    }\n\n    private final @Nullable Element componentsElement;\n    private final Map<String, Element> templates;\n\n    private final Deque<ExpansionFrame> expansionStack = new ArrayDeque<>();\n\n    protected UIModel(@Nullable Element componentsElement, Map<String, Element> templates) {\n        this.componentsElement = componentsElement;\n        this.templates = templates;\n    }\n\n    protected UIModel(Element docElement) {\n        docElement.normalize();\n        if (!docElement.getNodeName().equals(\"owo-ui\")) {\n            throw new UIModelParsingException(\"Missing 'owo-ui' root element\");\n        }\n\n        final var children = UIParsing.childElements(docElement);\n        if (children.containsKey(\"components\")) {\n            var componentsList = UIParsing.<Element>allChildrenOfType(children.get(\"components\"), Node.ELEMENT_NODE);\n            if (componentsList.size() == 1) {\n                this.componentsElement = componentsList.get(0);\n            } else {\n                throw new UIModelParsingException(\"Invalid number of children in 'components' element - a single child must be declared\");\n            }\n        } else {\n            this.componentsElement = null;\n        }\n\n        this.templates = UIParsing.get(children, \"templates\", element -> {\n            var templateChildren = element.getChildNodes();\n            var templates = new HashMap<String, Element>();\n\n            for (int i = 0; i < templateChildren.getLength(); i++) {\n                var child = templateChildren.item(i);\n                if (child.getNodeType() != Node.ELEMENT_NODE) continue;\n\n                var childElement = (Element) child;\n\n                if (childElement.getNodeName().equals(\"template\")) {\n                    UIParsing.expectAttributes(childElement, \"name\");\n                    templates.put(childElement.getAttribute(\"name\"), childElement);\n                } else {\n                    templates.put(childElement.getNodeName(), childElement);\n                }\n            }\n\n            return templates;\n        }).orElseGet(HashMap::new);\n    }\n\n    /**\n     * Load the UI model declared in the given file. If the file cannot\n     * be found or an XML parsing error occurs, null is\n     * returned and the error is logged\n     *\n     * @param path The file to read from\n     * @return The parsed UI model\n     */\n    public static @Nullable UIModel load(Path path) {\n        try (var in = Files.newInputStream(path)) {\n            return load(in);\n        } catch (Exception error) {\n            Owo.LOGGER.warn(\"Could not load UI model from file {}\", path, error);\n            return null;\n        }\n    }\n\n    /**\n     * Load the UI model declared in the XML document\n     * encoded by the given input stream. Contrary to {@link #load(Path)},\n     * this method throws if a parsing error occurs\n     *\n     * @param stream The input stream to decode and read\n     * @return The parsed UI model\n     */\n    public static UIModel load(InputStream stream) throws ParserConfigurationException, IOException, SAXException, UIModelParsingException {\n        return new UIModel(DOCUMENT_BUILDER.parse(stream).getDocumentElement());\n    }\n\n    /**\n     * Create a UI adapter which contains the component hierarchy\n     * declared by this UI model, attached to the given screen.\n     * <p>\n     * If there are components in your hierarchy you need to modify in\n     * code after the main hierarchy has been parsed, give them an id\n     * and look them up via {@link ParentUIComponent#childById(Class, String)}\n     *\n     * @param expectedRootComponentClass The class the created root component is expected to have.\n     *                                   Should this be violated, an exception is thrown. If there\n     *                                   are no specific expectations about the type of\n     *                                   root component to create, pass {@link UIComponent}\n     */\n    public <T extends ParentUIComponent> OwoUIAdapter<T> createAdapter(Class<T> expectedRootComponentClass, Screen screen) {\n        return OwoUIAdapter.create(screen, (horizontalSizing, verticalSizing) -> this.parseComponentTree(expectedRootComponentClass));\n    }\n\n    /**\n     * Create a UI adapter which contains the component hierarchy\n     * declared by this UI model, without the context of a screen\n     * <p>\n     * If there are components in your hierarchy you need to modify in\n     * code after the main hierarchy has been parsed, give them an id\n     * and look them up via {@link ParentUIComponent#childById(Class, String)}\n     *\n     * @param expectedRootComponentClass The class the created root component is expected to have.\n     *                                   Should this be violated, an exception is thrown. If there\n     *                                   are no specific expectations about the type of\n     *                                   root component to create, pass {@link UIComponent}\n     */\n    public <T extends ParentUIComponent> OwoUIAdapter<T> createAdapterWithoutScreen(int x, int y, int width, int height, Class<T> expectedRootComponentClass) {\n        return OwoUIAdapter.createWithoutScreen(x, y, width, height, (horizontalSizing, verticalSizing) -> this.parseComponentTree(expectedRootComponentClass));\n    }\n\n    /**\n     * Attempt to parse the given XMl element into a component,\n     * expanding any templates encountered. If the XML does\n     * not describe a valid component, a {@link UIModelParsingException}\n     * may be thrown\n     *\n     * @param expectedClass    The class the parsed component is expected to\n     *                         have. Should this be violated, an exception is\n     *                         thrown. If there are no specific expectations about\n     *                         the type of component to parse, pass {@link UIComponent}\n     * @param componentElement The XML element represented the\n     *                         component to parse.\n     * @return The parsed component\n     */\n    @SuppressWarnings(\"unchecked\")\n    public <T extends UIComponent> T parseComponent(Class<T> expectedClass, Element componentElement) {\n        if (componentElement.getNodeName().equals(\"template\")) {\n            var templateName = componentElement.getAttribute(\"name\").strip();\n            if (templateName.isEmpty()) {\n                throw new UIModelParsingException(\"Template element is missing 'name' attribute\");\n            }\n\n            var templateParams = new HashMap<String, String>();\n            var childParams = new HashMap<String, Element>();\n            for (var element : UIParsing.<Element>allChildrenOfType(componentElement, Node.ELEMENT_NODE)) {\n                if (element.getNodeName().equals(\"child\")) {\n                    childParams.put(\n                            element.getAttribute(\"id\"),\n                            UIParsing.<Element>allChildrenOfType(element, Node.ELEMENT_NODE).get(0)\n                    );\n                } else {\n                    templateParams.put(element.getNodeName(), element.getTextContent());\n                }\n            }\n\n            return this.expandTemplate(expectedClass, templateName, templateParams::get, childParams::get);\n        }\n\n        var component = UIParsing.getFactory(componentElement).apply(componentElement);\n        component.parseProperties(this, componentElement, UIParsing.childElements(componentElement));\n\n        if (!expectedClass.isAssignableFrom(component.getClass())) {\n            var idString = componentElement.hasAttribute(\"id\")\n                    ? \" with id '\" + componentElement.getAttribute(\"id\") + \"'\"\n                    : \"\";\n\n            throw new IncompatibleUIModelException(\n                    \"Expected component '\" + componentElement.getNodeName() + \"'\"\n                            + idString + \" to be a \" + expectedClass.getSimpleName()\n                            + \", but it is a \" + component.getClass().getSimpleName()\n            );\n        }\n\n        return (T) component;\n    }\n\n    /**\n     * Expand a template into a component, applying\n     * parameter mappings by invoking the given mapping\n     * function and creating template children using the given\n     * child supplier\n     *\n     * @param expectedClass     The class the expanded template is expected to\n     *                          have. Should this be violated, an exception is\n     *                          thrown. If there are no specific expectations about\n     *                          the type of component to create, pass {@link UIComponent}\n     * @param name              The name of the template to expand\n     * @param parameterSupplier The parameter mapping function to invoke\n     *                          for each parameter encountered in the template\n     * @param childSupplier     The template child mapping function to invoke\n     *                          for each template child the target template defines\n     * @return The expanded template parsed into a component\n     */\n    @SuppressWarnings(\"unchecked\")\n    public <T extends UIComponent> T expandTemplate(Class<T> expectedClass, String name, Function<String, String> parameterSupplier, Function<String, Element> childSupplier) {\n        if (this.expansionStack.isEmpty()) {\n            this.expansionStack.push(new ExpansionFrame(parameterSupplier, childSupplier));\n        } else {\n            final var currentFrame = this.expansionStack.peek();\n            this.expansionStack.push(new ExpansionFrame(\n                    this.cascadeIfNull(currentFrame.parameterSupplier, parameterSupplier),\n                    this.cascadeIfNull(currentFrame.childSupplier, childSupplier)\n            ));\n        }\n\n        Element template;\n        var splitTemplateName = name.split(\"@\");\n        if (splitTemplateName.length == 2) {\n            var modelReference = UIModelLoader.get(Identifier.parse(splitTemplateName[1]));\n            if (modelReference == null) {\n                throw new UIModelParsingException(\"Unknown UI model \" + splitTemplateName[1] + \", referenced by template \" + splitTemplateName[0]);\n            }\n\n            template = modelReference.templates.get(splitTemplateName[0]);\n        } else {\n            template = this.templates.get(name);\n        }\n\n        if (template == null) {\n            throw new UIModelParsingException(\"Unknown template '\" + name + \"'\");\n        } else {\n            template = (Element) template.cloneNode(true);\n        }\n\n        this.expandChildren(template);\n        this.applySubstitutions(template);\n\n        final var component = this.parseComponent(UIComponent.class, UIParsing.<Element>allChildrenOfType(template, Node.ELEMENT_NODE).get(0));\n        if (!expectedClass.isAssignableFrom(component.getClass())) {\n            throw new IncompatibleUIModelException(\n                    \"Expected template '\" + name + \"'\"\n                            + \" to expand into a \" + expectedClass.getSimpleName()\n                            + \", but it expanded into a \" + component.getClass().getSimpleName()\n            );\n        }\n\n        this.expansionStack.pop();\n        return (T) component;\n    }\n\n    /**\n     * Expand a template into a component, applying\n     * the given parameter mappings. If the template defines child\n     * elements, this method will most likely fail because\n     * parameters for those can only be provided in XML\n     *\n     * @param expectedClass The class the expanded template is expected to\n     *                      have. Should this be violated, an exception is\n     *                      thrown. If there are no specific expectations about\n     *                      the type of component to create, pass {@link UIComponent}\n     * @param name          The name of the template to expand\n     * @param parameters    The parameter mappings to apply while\n     *                      expanding the template\n     * @return The expanded template parsed into a component\n     */\n    public <T extends UIComponent> T expandTemplate(Class<T> expectedClass, String name, Map<String, String> parameters) {\n        return this.expandTemplate(expectedClass, name, parameters::get, s -> null);\n    }\n\n    protected <T extends ParentUIComponent> T parseComponentTree(Class<T> expectedRootComponentClass) {\n        if (this.componentsElement == null) {\n            throw new IncompatibleUIModelException(\"This UI model does not declare a component tree and can thus only provide templates\");\n        }\n\n        var documentComponent = this.parseComponent(expectedRootComponentClass, this.componentsElement);\n        documentComponent.sizing(Sizing.fill(100), Sizing.fill(100));\n        return documentComponent;\n    }\n\n    protected void applySubstitutions(Element template) {\n        var parameterSupplier = this.expansionStack.peek().parameterSupplier;\n        Function<MatchResult, String> replacer = matchResult -> {\n            final var paramName = matchResult.group().substring(2, matchResult.group().length() - 2);\n            final var substitution = parameterSupplier.apply(paramName);\n            if (substitution == null) {\n                throw new IncompatibleUIModelException(\"No substitution provided for template parameter '\" + paramName + \"'\");\n            }\n\n            return Matcher.quoteReplacement(substitution);\n        };\n\n        for (var child : UIParsing.<Element>allChildrenOfType(template, Node.ELEMENT_NODE)) {\n            for (var node : UIParsing.<Text>allChildrenOfType(child, Node.TEXT_NODE)) {\n                var textContent = node.getTextContent();\n                node.setTextContent(PARAMETER_PATTERN.matcher(textContent).replaceAll(replacer));\n            }\n\n            for (int i = 0; i < child.getAttributes().getLength(); i++) {\n                var attr = (Attr) child.getAttributes().item(i);\n                attr.setValue(PARAMETER_PATTERN.matcher(attr.getValue()).replaceAll(replacer));\n            }\n            applySubstitutions(child);\n        }\n    }\n\n    protected void expandChildren(Element template) {\n        final var childSupplier = this.expansionStack.peek().childSupplier;\n\n        for (var child : UIParsing.<Element>allChildrenOfType(template, Node.ELEMENT_NODE)) {\n            if (child.getNodeName().equals(\"template-child\")) {\n                var childId = child.getAttribute(\"id\");\n\n                var expanded = childSupplier.apply(childId);\n                if (expanded != null) {\n                    expanded = (Element) expanded.cloneNode(true);\n                    var expandedChildren = UIParsing.childElements(expanded);\n\n                    for (var element : UIParsing.<Element>allChildrenOfType(child, Node.ELEMENT_NODE)) {\n                        if (expandedChildren.containsKey(element.getTagName())) continue;\n                        expanded.appendChild(element);\n                    }\n\n                    template.replaceChild(expanded, child);\n                } else {\n                    throw new IncompatibleUIModelException(\"No expansion provided for template child '\" + childId + \"'\");\n                }\n            }\n\n            expandChildren(child);\n        }\n    }\n\n    protected <T, S> Function<T, S> cascadeIfNull(Function<T, S> first, Function<T, S> second) {\n        return t -> {\n            var firstValue = first.apply(t);\n            return firstValue == null ? second.apply(t) : firstValue;\n        };\n    }\n\n    private record ExpansionFrame(Function<String, String> parameterSupplier,\n                                  Function<String, Element> childSupplier) {}\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/parsing/UIModelLoader.java",
    "content": "package io.wispforest.owo.ui.parsing;\n\nimport blue.endless.jankson.Jankson;\nimport blue.endless.jankson.JsonGrammar;\nimport blue.endless.jankson.JsonPrimitive;\nimport blue.endless.jankson.api.SyntaxError;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ops.TextOps;\nimport net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport net.minecraft.server.packs.resources.ResourceManagerReloadListener;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\nimport org.xml.sax.SAXException;\n\nimport javax.xml.parsers.ParserConfigurationException;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class UIModelLoader implements ResourceManagerReloadListener, IdentifiableResourceReloadListener {\n\n    private static final Map<Identifier, UIModel> LOADED_MODELS = new HashMap<>();\n\n    private static final Jankson JANKSON = Jankson.builder()\n            .registerSerializer(Path.class, (path, marshaller) -> JsonPrimitive.of(path.toString()))\n            .registerSerializer(Identifier.class, (identifier, marshaller) -> new JsonPrimitive(identifier.toString()))\n            .build();\n\n    private static final Path HOT_RELOAD_LOCATIONS_PATH = FabricLoader.getInstance().getConfigDir().resolve(\"owo_ui_hot_reload_locations.json5\");\n    private static final Map<Identifier, Path> HOT_RELOAD_LOCATIONS = new HashMap<>();\n\n    private static boolean loadedOnce = false;\n\n    /**\n     * Get the most up-to-date version of the UI model specified\n     * by the given identifier. If debug mod is enabled and a hot reload\n     * location has been configured by the user for this specific model,\n     * a hot reload will be attempted\n     *\n     * @return The most up-to-date version of the requested model, or\n     * the result of {@link #getPreloaded(Identifier)} if the hot reload\n     * fails for any reason\n     */\n    public static @Nullable UIModel get(Identifier id) {\n        if (Owo.DEBUG && HOT_RELOAD_LOCATIONS.containsKey(id)) {\n            try (var stream = Files.newInputStream(HOT_RELOAD_LOCATIONS.get(id))) {\n                return UIModel.load(stream);\n            } catch (ParserConfigurationException | IOException | SAXException e) {\n                Minecraft.getInstance().player.displayClientMessage(TextOps.concat(Owo.PREFIX, TextOps.withFormatting(\"hot ui model reload failed, check the log for details\", ChatFormatting.RED)), false);\n                Owo.LOGGER.error(\"Hot UI model reload failed\", e);\n            }\n        }\n\n        return getPreloaded(id);\n    }\n\n    /**\n     * Fetch the UI model specified by the given identifier from the\n     * cache created during the last resource reload\n     */\n    public static @Nullable UIModel getPreloaded(Identifier id) {\n        return LOADED_MODELS.getOrDefault(id, null);\n    }\n\n    /**\n     * Set the path from which to attempt a hot reload when the UI\n     * model with the given identifier is requested through {@link #get(Identifier)}.\n     * <p>\n     * Call with a {@code null} path to clear\n     */\n    public static void setHotReloadPath(Identifier modelId, @Nullable Path reloadPath) {\n        if (reloadPath != null) {\n            HOT_RELOAD_LOCATIONS.put(modelId, reloadPath);\n        } else {\n            HOT_RELOAD_LOCATIONS.remove(modelId);\n        }\n\n        try {\n            Files.writeString(HOT_RELOAD_LOCATIONS_PATH, JANKSON.toJson(HOT_RELOAD_LOCATIONS).toJson(JsonGrammar.JSON5));\n        } catch (IOException e) {\n            Owo.LOGGER.warn(\"Could not save hot reload locations\", e);\n        }\n    }\n\n    public static @Nullable Path getHotReloadPath(Identifier modelId) {\n        return HOT_RELOAD_LOCATIONS.get(modelId);\n    }\n\n    public static Set<Identifier> allLoadedModels() {\n        return Collections.unmodifiableSet(LOADED_MODELS.keySet());\n    }\n\n    @Override\n    public Identifier getFabricId() {\n        return Owo.id(\"ui-model-loader\");\n    }\n\n    @Override\n    public void onResourceManagerReload(ResourceManager manager) {\n        LOADED_MODELS.clear();\n\n        manager.listResources(\"owo_ui\", identifier -> identifier.getPath().endsWith(\".xml\")).forEach((resourceId, resource) -> {\n            try {\n                var modelId = Identifier.fromNamespaceAndPath(\n                        resourceId.getNamespace(),\n                        resourceId.getPath().substring(7, resourceId.getPath().length() - 4)\n                );\n\n                LOADED_MODELS.put(modelId, UIModel.load(resource.open()));\n            } catch (ParserConfigurationException | IOException | SAXException e) {\n                Owo.LOGGER.error(\"Couldn't parse UI model {}\", resourceId, e);\n            }\n        });\n\n        loadedOnce = true;\n    }\n\n    @ApiStatus.Internal\n    public static boolean hasCompletedInitialLoad() {\n        return loadedOnce;\n    }\n\n    static {\n        if (Owo.DEBUG && Files.exists(HOT_RELOAD_LOCATIONS_PATH)) {\n            try (var stream = Files.newInputStream(HOT_RELOAD_LOCATIONS_PATH)) {\n                var associations = JANKSON.load(stream);\n                associations.forEach((key, value) -> {\n                    if (!(value instanceof JsonPrimitive primitive)) return;\n                    HOT_RELOAD_LOCATIONS.put(Identifier.parse(key), Path.of(primitive.asString()));\n                });\n            } catch (IOException | SyntaxError ignored) {}\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/parsing/UIModelParsingException.java",
    "content": "package io.wispforest.owo.ui.parsing;\n\n/**\n * Describes an error that happened during instantiation\n * of a UIModel, most commonly due to improperly formatted XML\n * or XML which describes invalid values\n */\npublic class UIModelParsingException extends RuntimeException {\n\n    public UIModelParsingException(String message) {\n        super(message);\n    }\n\n    public UIModelParsingException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/parsing/UIParsing.java",
    "content": "package io.wispforest.owo.ui.parsing;\n\nimport io.wispforest.owo.ui.component.*;\nimport io.wispforest.owo.ui.container.*;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport net.minecraft.IdentifierException;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.item.ItemStack;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\n\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n/**\n * A utility class containing the component factory registry\n * as well as some utility functions to ease model parsing\n */\npublic class UIParsing {\n\n    private static final Map<String, Function<Element, UIComponent>> COMPONENT_FACTORIES = new HashMap<>();\n\n    /**\n     * @deprecated In order to more properly separate factories added by different\n     * mods, use {@link #registerFactory(Identifier, Function)}, which takes an\n     * identifier instead\n     */\n    @ApiStatus.Internal\n    public static void registerFactory(String componentTagName, Function<Element, UIComponent> factory) {\n        if (COMPONENT_FACTORIES.containsKey(componentTagName)) {\n            throw new IllegalStateException(\"A component factory with name \" + componentTagName + \" is already registered\");\n        }\n\n        COMPONENT_FACTORIES.put(componentTagName, factory);\n    }\n\n    /**\n     * Register a factory used to create components from XML elements.\n     * Most factories will only consider the tag name of the element,\n     * but more context can be extracted from the passed element\n     *\n     * @param componentId The identifier under which to register the component,\n     *                    which (separated by a period instead of a colon) is used\n     *                    as the tag name for which this factory gets invoked\n     * @param factory     The factory to register\n     */\n    public static void registerFactory(Identifier componentId, Function<Element, UIComponent> factory) {\n        registerFactory(componentId.getNamespace() + \".\" + componentId.getPath(), factory);\n    }\n\n    /**\n     * Get the appropriate component factory for the given\n     * XML element. An exception is thrown if none is registered\n     *\n     * @param element The element representing the component to be parsed\n     * @return The matching factory\n     * @throws UIModelParsingException If there is no registered factory\n     *                                 capable of parsing the given element\n     */\n    public static Function<Element, UIComponent> getFactory(Element element) {\n        var factory = COMPONENT_FACTORIES.get(element.getNodeName());\n        if (factory == null) {\n            throw new UIModelParsingException(\"Unknown component type: \" + element.getNodeName());\n        }\n\n        return factory;\n    }\n\n    /**\n     * Extract all children of the given element which match the expected type\n     *\n     * @param type The type of child nodes to extract\n     * @param <T>  The class to cast the extracted nodes to\n     * @return A list of all children of {@code element} which have a type of {@code type}\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Node> List<T> allChildrenOfType(Element element, short type) {\n        var list = new ArrayList<T>();\n        for (int i = 0; i < element.getChildNodes().getLength(); i++) {\n            var child = element.getChildNodes().item(i);\n            if (child.getNodeType() != type) continue;\n            list.add((T) child);\n        }\n        return list;\n    }\n\n    /**\n     * Extract all child elements of the given element into a map from tag\n     * name to element. An exception is thrown if a tag name appears twice\n     *\n     * @return All element children of {@code element} mapped from\n     * tag name to element\n     * @throws UIModelParsingException If two or more children share the same tag name\n     */\n    public static Map<String, Element> childElements(Element element) {\n        var children = element.getChildNodes();\n        var map = new HashMap<String, Element>();\n\n        for (int i = 0; i < children.getLength(); i++) {\n            var child = children.item(i);\n            if (child.getNodeType() != Node.ELEMENT_NODE) continue;\n\n            if (map.containsKey(child.getNodeName())) {\n                throw new UIModelParsingException(\"Duplicate child \" + child.getNodeName() + \" in element \" + element.getNodeName());\n            }\n\n            map.put(child.getNodeName(), (Element) child);\n        }\n\n        return map;\n    }\n\n    /**\n     * Tries to interpret the text content of the\n     * given node as a signed integer\n     *\n     * @throws UIModelParsingException If the text content does not\n     *                                 represent a valid signed integer\n     */\n    public static int parseSignedInt(Node node) {\n        return parseInt(node, true);\n    }\n\n    /**\n     * Tries to interpret the text content of the\n     * given node as an unsigned integer\n     *\n     * @throws UIModelParsingException If the text content does not\n     *                                 represent a valid unsigned integer\n     */\n    public static int parseUnsignedInt(Node node) {\n        return parseInt(node, false);\n    }\n\n    /**\n     * Tries to interpret the text content of the\n     * given node as a floating-point number\n     *\n     * @throws UIModelParsingException If the text content does not\n     *                                 represent a valid floating point number\n     */\n    public static float parseFloat(Node node) {\n        var data = node.getTextContent().strip();\n        if (data.matches(\"-?\\\\d+(\\\\.\\\\d+)?\")) {\n            return Float.parseFloat(data);\n        } else {\n            throw new UIModelParsingException(\"Invalid value '\" + data + \"', expected a floating point number\");\n        }\n    }\n\n    /**\n     * Tries to interpret the text content of the\n     * given node as a double-precision floating-point number\n     *\n     * @throws UIModelParsingException If the text content does not\n     *                                 represent a valid floating point number\n     */\n    public static double parseDouble(Node node) {\n        var data = node.getTextContent().strip();\n        if (data.matches(\"-?\\\\d+(\\\\.\\\\d+)?\")) {\n            return Double.parseDouble(data);\n        } else {\n            throw new UIModelParsingException(\"Invalid value '\" + data + \"', expected a double-precision floating point number\");\n        }\n    }\n\n    /**\n     * Interprets the text content of the\n     * given node as a boolean - more specifically this\n     * method returns {@code true} if and only if the text content\n     * equals {@code true}, without respecting letter case\n     */\n    public static boolean parseBool(Node node) {\n        return node.getTextContent().strip().equalsIgnoreCase(\"true\");\n    }\n\n    /**\n     * Tries to interpret the text content of the\n     * given node as an identifier\n     *\n     * @throws UIModelParsingException If the text content does not\n     *                                 represent a valid identifier\n     */\n    public static Identifier parseIdentifier(Node node) {\n        try {\n            return Identifier.parse(node.getTextContent().strip());\n        } catch (IdentifierException exception) {\n            throw new UIModelParsingException(\"Invalid identifier '\" + node.getTextContent() + \"'\", exception);\n        }\n    }\n\n    /**\n     * Interprets the text content of the\n     * given element as text. If the {@code translate}\n     * attribute is set to {@code true}, the content is\n     * interpreted as a translation key - otherwise it is\n     * returned literally\n     */\n    public static Component parseText(Element element) {\n        return element.getAttribute(\"translate\").equalsIgnoreCase(\"true\")\n                ? Component.translatable(element.getTextContent())\n                : Component.literal(element.getTextContent());\n    }\n\n    public static <E extends Enum<E>> Function<Element, E> parseEnum(Class<E> enumClass) {\n        return element -> {\n            var name = element.getTextContent().strip().toUpperCase(Locale.ROOT).replace('-', '_');\n            for (var value : enumClass.getEnumConstants()) {\n                if (Objects.equals(name, value.name())) return value;\n            }\n\n            throw new UIModelParsingException(\"No such constant \" + name + \" in enum \" + enumClass.getSimpleName());\n        };\n    }\n\n    /**\n     * Parse the property indicated by {@code key} into an object of type {@code T}\n     *\n     * @param properties The map containing all available properties\n     * @param key        The key of the property to parse\n     * @param parser     The parsing function to use\n     * @param <T>        The type of object to parse\n     * @return An optional containing the parsed property, or an empty optional\n     * if the requested property was not contained in the given map\n     */\n    public static <T, E extends Node> Optional<T> get(Map<String, E> properties, String key, Function<E, T> parser) {\n        if (!properties.containsKey(key)) return Optional.empty();\n        return Optional.of(parser.apply(properties.get(key)));\n    }\n\n    /**\n     * Parse the property indicated by {@code key} into an object of type {@code T}\n     * and apply the given function if it was present\n     *\n     * @param properties The map containing all available properties\n     * @param key        The key of the property to parse\n     * @param parser     The parsing function to use\n     * @param consumer   The function to apply if the property was present\n     *                   in the map and successfully parsed\n     * @param <T>        The type of object to parse\n     */\n    public static <T, E extends Node> void apply(Map<String, E> properties, String key, Function<E, T> parser, Consumer<T> consumer) {\n        if (!properties.containsKey(key)) return;\n        consumer.accept(parser.apply(properties.get(key)));\n    }\n\n    /**\n     * Verify that all the given attributes are present\n     * on the given element and throw if one is missing\n     *\n     * @param element    The element to verify\n     * @param attributes The attributes to verify\n     */\n    public static void expectAttributes(Element element, String... attributes) {\n        for (var attr : attributes) {\n            if (!element.hasAttribute(attr)) {\n                throw new UIModelParsingException(\"Element '\" + element.getNodeName() + \"' is missing attribute '\" + attr + \"'\");\n            }\n        }\n    }\n\n    /**\n     * Verify that all the given elements are present\n     * as children of the given element and throw if one is missing\n     *\n     * @param element  The element to verify\n     * @param children The children of that element\n     * @param expected The expected child elements\n     */\n    public static void expectChildren(Element element, Map<String, Element> children, String... expected) {\n        for (var childName : expected) {\n            if (!children.containsKey(childName)) {\n                throw new UIModelParsingException(\"Element '\" + element.getNodeName() + \"' is missing element '\" + childName + \"'\");\n            }\n        }\n    }\n\n    protected static int parseInt(Node node, boolean allowNegative) {\n        var data = node.getTextContent().strip();\n        if (data.matches((allowNegative ? \"-?\" : \"\") + \"\\\\d+\")) {\n            return Integer.parseInt(data);\n        } else {\n            throw new UIModelParsingException(\"Invalid value '\" + data + \"', expected \" + (allowNegative ? \"\" : \"positive\") + \" integer\");\n        }\n    }\n\n    static {\n        // Layout\n        registerFactory(\"flow-layout\", FlowLayout::parse);\n        registerFactory(\"grid-layout\", GridLayout::parse);\n        registerFactory(\"stack-layout\", element -> UIContainers.stack(Sizing.content(), Sizing.content()));\n\n        // Container\n        registerFactory(\"scroll\", ScrollContainer::parse);\n        registerFactory(\"collapsible\", CollapsibleContainer::parse);\n        registerFactory(\"draggable\", element -> UIContainers.draggable(Sizing.content(), Sizing.content(), null));\n\n        // Textures\n        registerFactory(\"sprite\", SpriteComponent::parse);\n        registerFactory(\"texture\", TextureComponent::parse);\n\n        // Game Objects\n        registerFactory(\"entity\", EntityComponent::parse);\n        registerFactory(\"item\", element -> UIComponents.item(ItemStack.EMPTY));\n        registerFactory(\"block\", BlockComponent::parse);\n\n        // Widgets\n        registerFactory(\"label\", element -> UIComponents.label(Component.empty()));\n        registerFactory(\"box\", element -> UIComponents.box(Sizing.content(), Sizing.content()));\n        registerFactory(\"button\", element -> UIComponents.button(Component.empty(), (ButtonComponent button) -> {}));\n        registerFactory(\"checkbox\", element -> UIComponents.checkbox(Component.empty()));\n        registerFactory(\"text-box\", element -> UIComponents.textBox(Sizing.content()));\n        registerFactory(\"text-area\", element -> UIComponents.textArea(Sizing.content(), Sizing.content()));\n        registerFactory(\"slider\", element -> UIComponents.slider(Sizing.content()));\n        registerFactory(\"discrete-slider\", DiscreteSliderComponent::parse);\n        registerFactory(\"dropdown\", element -> UIComponents.dropdown(Sizing.content()));\n        registerFactory(\"color-picker\", element -> new ColorPickerComponent());\n        registerFactory(\"slim-slider\", SlimSliderComponent::parse);\n        registerFactory(\"small-checkbox\", element -> new SmallCheckboxComponent());\n        registerFactory(\"spacer\", SpacerComponent::parse);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/BlockElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.platform.Lighting;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport com.mojang.math.Axis;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.LightTexture;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;\nimport net.minecraft.client.renderer.state.CameraRenderState;\nimport net.minecraft.client.renderer.texture.OverlayTexture;\nimport net.minecraft.world.level.block.RenderShape;\nimport net.minecraft.world.level.block.state.BlockState;\nimport org.jetbrains.annotations.Nullable;\n\npublic record BlockElementRenderState(\n    BlockState state,\n    @Nullable BlockEntityRenderState entity,\n    ScreenRectangle bounds,\n    ScreenRectangle scissorArea\n) implements PictureInPictureRenderState {\n\n    @Override\n    public int x0() {\n        return this.bounds.left();\n    }\n\n    @Override\n    public int x1() {\n        return this.bounds.right();\n    }\n\n    @Override\n    public int y0() {\n        return this.bounds.top();\n    }\n\n    @Override\n    public int y1() {\n        return this.bounds.bottom();\n    }\n\n    @Override\n    public float scale() {\n        return 1;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        return this.scissorArea != null ? this.scissorArea.intersection(this.bounds) : this.bounds;\n    }\n\n    public static class Renderer extends PictureInPictureRenderer<BlockElementRenderState> {\n\n        public Renderer(MultiBufferSource.BufferSource vertexConsumers) {\n            super(vertexConsumers);\n        }\n\n        @Override\n        public Class<BlockElementRenderState> getRenderStateClass() {\n            return BlockElementRenderState.class;\n        }\n\n        @Override\n        @SuppressWarnings(\"NonAsciiCharacters\")\n        protected void renderToTexture(BlockElementRenderState state, PoseStack matrices) {\n            Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ENTITY_IN_UI);\n\n            var width = state.bounds.width();\n            var height = state.bounds.height();\n\n            matrices.translate(0, -height / 2f, 100);\n            matrices.scale(40 * width / 64f, -40 * height / 64f, -40);\n\n            matrices.mulPose(Axis.XP.rotationDegrees(30));\n            matrices.mulPose(Axis.YP.rotationDegrees(45 + 180));\n\n            matrices.translate(-.5, -.5, -.5);\n\n            if (state.state.getRenderShape() != RenderShape.INVISIBLE) {\n                Minecraft.getInstance().getBlockRenderer().renderSingleBlock(\n                    state.state, matrices, bufferSource,\n                    LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY\n                );\n            }\n\n            if (state.entity != null) {\n                var медведь = Minecraft.getInstance().getBlockEntityRenderDispatcher().getRenderer(state.entity);\n                if (медведь != null) {\n                    var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();\n                    медведь.submit(state.entity, matrices, dispatcher.getSubmitNodeStorage(), new CameraRenderState());\n                    dispatcher.renderAllFeatures();\n                }\n            }\n        }\n\n        @Override\n        protected String getTextureLabel() {\n            return \"owo-ui_block\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/BlurQuadElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.google.common.collect.MapMaker;\nimport com.mojang.blaze3d.buffers.GpuBufferSlice;\nimport com.mojang.blaze3d.buffers.Std140Builder;\nimport com.mojang.blaze3d.buffers.Std140SizeCalculator;\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.pipeline.RenderTarget;\nimport com.mojang.blaze3d.pipeline.TextureTarget;\nimport com.mojang.blaze3d.systems.RenderSystem;\nimport com.mojang.blaze3d.textures.GpuTextureView;\nimport com.mojang.blaze3d.vertex.VertexConsumer;\nimport io.wispforest.owo.ui.core.OwoUIPipelines;\nimport io.wispforest.owo.ui.event.ClientRenderCallback;\nimport io.wispforest.owo.ui.event.WindowResizeCallback;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.TextureSetup;\nimport net.minecraft.client.gui.render.state.GuiElementRenderState;\nimport net.minecraft.client.renderer.DynamicUniformStorage;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2i;\n\nimport java.nio.ByteBuffer;\nimport java.util.Map;\n\npublic record BlurQuadElementRenderState(\n    RenderPipeline pipeline,\n    Matrix3x2f pose,\n    ScreenRectangle bounds,\n    ScreenRectangle scissorArea,\n    TextureSetup textureSetup\n) implements GuiElementRenderState {\n\n    public static Uniforms uniforms;\n    public static RenderTarget input;\n    public static GpuTextureView inputView;\n\n    @ApiStatus.Internal\n    public static void initialize(Minecraft client) {\n        uniforms = new Uniforms();\n\n        var window = client.getWindow();\n\n        input = new TextureTarget(\"owo_blur_input\", window.getWidth(), window.getHeight(), false);\n        inputView = RenderSystem.getDevice().createTextureView(input.getColorTexture());\n\n        WindowResizeCallback.EVENT.register((innerClient, innerWindow) -> {\n            if (input == null) return;\n            input.resize(innerWindow.getWidth(), innerWindow.getHeight());\n\n            inputView.close();\n            inputView = RenderSystem.getDevice().createTextureView(input.getColorTexture());\n        });\n\n        ClientRenderCallback.AFTER.register($ -> {\n            uniforms.clear();\n        });\n    }\n\n    @ApiStatus.Internal\n    public BlurQuadElementRenderState {}\n\n    public BlurQuadElementRenderState(Matrix3x2f pose, ScreenRectangle bounds, ScreenRectangle scissorArea, int directions, float quality, float size) {\n        this(OwoUIPipelines.GUI_BLUR, pose, bounds, scissorArea, createTextureSetup(directions, quality, size));\n    }\n\n    @Override\n    public void buildVertices(VertexConsumer vertices) {\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.left(), (float) this.bounds.top());\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.left(), (float) this.bounds.bottom());\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.right(), (float) this.bounds.bottom());\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.right(), (float) this.bounds.top());\n    }\n\n    @Override\n    public RenderPipeline pipeline() {\n        return this.pipeline;\n    }\n\n    @Override\n    public TextureSetup textureSetup() {\n        return this.textureSetup;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        return this.scissorArea != null ? this.scissorArea.intersection(this.bounds) : this.bounds;\n    }\n\n    // ---\n\n    private static final Map<TextureSetup, BlurSetup> blurSetups = new MapMaker().weakKeys().makeMap();\n\n    public static boolean hasBlurSetupFor(TextureSetup textureSetup) {\n        return blurSetups.containsKey(textureSetup);\n    }\n\n    public static @Nullable BlurSetup getBlurSetupOf(TextureSetup textureSetup) {\n        return blurSetups.get(textureSetup);\n    }\n\n    private static TextureSetup createTextureSetup(int directions, float quality, float size) {\n        var setup = TextureSetup.singleTexture(null, null);\n        blurSetups.put(setup, new BlurSetup(directions, quality, size));\n        return setup;\n    }\n\n    public record BlurSetup(int directions, float quality, float size) {}\n\n    // ---\n\n    public static class Uniforms {\n        public static final int SIZE = new Std140SizeCalculator().putVec2().putFloat().putFloat().putFloat().get();\n        private final DynamicUniformStorage<Value> storage = new DynamicUniformStorage<>(\"Blur Settings UBO\", SIZE, 4);\n\n        public void clear() {\n            this.storage.endFrame();\n        }\n\n        public GpuBufferSlice write(Vector2i inputResolution, int directions, float quality, float size) {\n            return this.storage.writeUniform(new Value(inputResolution, directions, quality, size));\n        }\n\n        @Environment(EnvType.CLIENT)\n        public record Value(Vector2i inputResolution, int directions, float quality, float size) implements DynamicUniformStorage.DynamicUniform {\n            @Override\n            public void write(ByteBuffer buffer) {\n                Std140Builder.intoBuffer(buffer)\n                    .putVec2(inputResolution.x, inputResolution.y)\n                    .putFloat(this.directions)\n                    .putFloat(this.quality)\n                    .putFloat(this.size);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/CircleElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.vertex.VertexConsumer;\nimport io.wispforest.owo.ui.core.Color;\nimport net.minecraft.client.gui.navigation.ScreenPosition;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.TextureSetup;\nimport net.minecraft.client.gui.render.state.GuiElementRenderState;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\n\npublic record CircleElementRenderState(\n    RenderPipeline pipeline,\n    Matrix3x2f pose,\n    ScreenRectangle scissorArea,\n    int centerX,\n    int centerY,\n    double angleFrom,\n    double angleTo,\n    int segments,\n    double radius,\n    Color color\n) implements GuiElementRenderState {\n    @Override\n    public void buildVertices(VertexConsumer vertices) {\n        double angleStep = Math.toRadians(this.angleTo - this.angleFrom) / this.segments;\n        int vColor = this.color.argb();\n\n        vertices.addVertexWith2DPose(this.pose, this.centerX, this.centerY).setColor(vColor);\n\n        for (int i = this.segments; i >= 0; i--) {\n            double theta = Math.toRadians(this.angleFrom) + i * angleStep;\n            vertices.addVertexWith2DPose(this.pose, (float) (this.centerX - Math.cos(theta) * this.radius), (float) (this.centerY - Math.sin(theta) * this.radius))\n                .setColor(vColor);\n        }\n    }\n\n    @Override\n    public RenderPipeline pipeline() {\n        return this.pipeline;\n    }\n\n    @Override\n    public TextureSetup textureSetup() {\n        return TextureSetup.noTexture();\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public ScreenRectangle bounds() {\n        var screenRect =  new ScreenRectangle(\n            new ScreenPosition((int) (this.centerX - this.radius), (int) (this.centerY - this.radius)),\n            (int) Math.ceil(this.radius * 2),\n            (int) Math.ceil(this.radius * 2)\n        ).transformMaxBounds(this.pose);\n\n        return this.scissorArea != null ? this.scissorArea.intersection(screenRect) : screenRect;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/CubeMapElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.systems.RenderSystem;\nimport com.mojang.blaze3d.textures.GpuTextureView;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.GuiRenderState;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.PanoramaRenderer;\nimport org.jetbrains.annotations.Nullable;\n\npublic record CubeMapElementRenderState(\n    PanoramaRenderer cubeMap,\n    boolean rotate,\n    ScreenRectangle bounds,\n    ScreenRectangle scissorArea\n) implements PictureInPictureRenderState {\n\n    public static OutputOverride outputOverride = null;\n\n    @Override\n    public int x0() {\n        return this.bounds.left();\n    }\n\n    @Override\n    public int x1() {\n        return this.bounds.right();\n    }\n\n    @Override\n    public int y0() {\n        return this.bounds.top();\n    }\n\n    @Override\n    public int y1() {\n        return this.bounds.bottom();\n    }\n\n    @Override\n    public float scale() {\n        return 1;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        return this.scissorArea != null ? this.scissorArea.intersection(this.bounds) : this.bounds;\n    }\n\n    public static class Renderer extends PictureInPictureRenderer<CubeMapElementRenderState> {\n\n        private static GuiGraphics dummyContext;\n\n        protected Renderer(MultiBufferSource.BufferSource vertexConsumers) {\n            super(vertexConsumers);\n        }\n\n        @Override\n        public Class<CubeMapElementRenderState> getRenderStateClass() {\n            return CubeMapElementRenderState.class;\n        }\n\n        @Override\n        protected void renderToTexture(CubeMapElementRenderState state, PoseStack matrices) {\n            if (dummyContext == null) {\n                dummyContext = new GuiGraphics(Minecraft.getInstance(), new GuiRenderState(), 0, 0);\n            }\n\n            dummyContext.guiRenderState.reset();\n\n            try {\n                CubeMapElementRenderState.outputOverride = new OutputOverride(\n                    RenderSystem.outputColorTextureOverride,\n                    RenderSystem.outputDepthTextureOverride,\n                    0xFF000000\n                );\n\n                state.cubeMap.render(dummyContext, state.bounds.width(), state.bounds.height(), state.rotate());\n            } finally {\n                CubeMapElementRenderState.outputOverride = null;\n            }\n        }\n\n        @Override\n        protected String getTextureLabel() {\n            return \"owo-ui_cubemap\";\n        }\n    }\n\n    public record OutputOverride(GpuTextureView color, GpuTextureView depth, int resetColor) {}\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/EntityElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.platform.Lighting;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.entity.EntityRenderDispatcher;\nimport net.minecraft.client.renderer.entity.state.EntityRenderState;\nimport net.minecraft.client.renderer.state.CameraRenderState;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix4f;\nimport org.joml.Quaternionf;\n\npublic record EntityElementRenderState(\n    EntityRenderState entityState,\n    Matrix4f transform,\n    ScreenRectangle bounds,\n    ScreenRectangle scissorArea\n) implements PictureInPictureRenderState {\n\n    @Override\n    public int x0() {\n        return this.bounds.left();\n    }\n\n    @Override\n    public int x1() {\n        return this.bounds.right();\n    }\n\n    @Override\n    public int y0() {\n        return this.bounds.top();\n    }\n\n    @Override\n    public int y1() {\n        return this.bounds.bottom();\n    }\n\n    @Override\n    public float scale() {\n        return 1;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        return this.scissorArea != null ? this.scissorArea.intersection(this.bounds) : this.bounds;\n    }\n\n    public static class Renderer extends PictureInPictureRenderer<EntityElementRenderState> {\n\n        private final EntityRenderDispatcher renderManager = Minecraft.getInstance().getEntityRenderDispatcher();\n\n        protected Renderer(MultiBufferSource.BufferSource vertexConsumers) {\n            super(vertexConsumers);\n        }\n\n        @Override\n        public Class<EntityElementRenderState> getRenderStateClass() {\n            return EntityElementRenderState.class;\n        }\n\n        @Override\n        protected void renderToTexture(EntityElementRenderState state, PoseStack matrices) {\n            Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ENTITY_IN_UI);\n\n            matrices.mulPose(state.transform);\n\n            var camera = new CameraRenderState();\n            camera.orientation = state.transform.invert().getUnnormalizedRotation(new Quaternionf());\n\n            var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();\n            this.renderManager.submit(state.entityState, camera, 0, 0, 0, matrices, dispatcher.getSubmitNodeStorage());\n            dispatcher.renderAllFeatures();\n        }\n\n        @Override\n        protected String getTextureLabel() {\n            return \"owo-ui_entity\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/GradientQuadElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.vertex.VertexConsumer;\nimport io.wispforest.owo.ui.core.Color;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.TextureSetup;\nimport net.minecraft.client.gui.render.state.GuiElementRenderState;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\n\npublic record GradientQuadElementRenderState(\n    RenderPipeline pipeline,\n    Matrix3x2f pose,\n    ScreenRectangle bounds,\n    ScreenRectangle scissorArea,\n    Color colorTL,\n    Color colorTR,\n    Color colorBL,\n    Color colorBR\n) implements GuiElementRenderState {\n\n    @Override\n    public void buildVertices(VertexConsumer vertices) {\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.left(), (float) this.bounds.top()).setColor(this.colorTL.argb());\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.left(), (float) this.bounds.bottom()).setColor(this.colorBL.argb());\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.right(), (float) this.bounds.bottom()).setColor(this.colorBR.argb());\n        vertices.addVertexWith2DPose(this.pose(), (float) this.bounds.right(), (float) this.bounds.top()).setColor(this.colorTR.argb());\n    }\n\n    @Override\n    public RenderPipeline pipeline() {\n        return this.pipeline;\n    }\n\n    @Override\n    public TextureSetup textureSetup() {\n        return TextureSetup.noTexture();\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        return this.scissorArea != null ? this.scissorArea.intersection(this.bounds) : this.bounds;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/LineElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.vertex.VertexConsumer;\nimport io.wispforest.owo.ui.core.Color;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.TextureSetup;\nimport net.minecraft.client.gui.render.state.GuiElementRenderState;\nimport org.joml.Matrix3x2f;\nimport org.joml.Vector2d;\n\npublic record LineElementRenderState(\n    RenderPipeline pipeline,\n    Matrix3x2f pose,\n    ScreenRectangle scissorArea,\n    int x0,\n    int y0,\n    int x1,\n    int y1,\n    double thiccness,\n    Color color\n) implements GuiElementRenderState {\n    @Override\n    public void buildVertices(VertexConsumer vertices) {\n        var offset = new Vector2d(this.x1 - this.x0, this.y1 - this.y0).perpendicular().normalize().mul(this.thiccness * .5d);\n\n        int vColor = this.color.argb();\n        vertices.addVertexWith2DPose(this.pose, (float) (x0 + offset.x), (float) (y0 + offset.y)).setColor(vColor);\n        vertices.addVertexWith2DPose(this.pose, (float) (x0 - offset.x), (float) (y0 - offset.y)).setColor(vColor);\n        vertices.addVertexWith2DPose(this.pose, (float) (x1 - offset.x), (float) (y1 - offset.y)).setColor(vColor);\n        vertices.addVertexWith2DPose(this.pose, (float) (x1 + offset.x), (float) (y1 + offset.y)).setColor(vColor);\n    }\n\n    @Override\n    public TextureSetup textureSetup() {\n        return TextureSetup.noTexture();\n    }\n\n    @Override\n    public ScreenRectangle bounds() {\n        return new ScreenRectangle(this.x0, this.y0, this.x1 - this.x0, this.y1 - this.y0);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/OwoItemElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.platform.Lighting;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.pip.PictureInPictureRenderer;\nimport net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;\nimport net.minecraft.client.renderer.LightTexture;\nimport net.minecraft.client.renderer.MultiBufferSource;\nimport net.minecraft.client.renderer.item.ItemStackRenderState;\nimport net.minecraft.client.renderer.texture.OverlayTexture;\nimport org.jetbrains.annotations.Nullable;\n\npublic record OwoItemElementRenderState(\n    ItemStackRenderState item,\n    ScreenRectangle bounds,\n    ScreenRectangle scissorArea\n) implements PictureInPictureRenderState {\n\n    @Override\n    public int x0() {\n        return this.bounds.left();\n    }\n\n    @Override\n    public int x1() {\n        return this.bounds.right();\n    }\n\n    @Override\n    public int y0() {\n        return this.bounds.top();\n    }\n\n    @Override\n    public int y1() {\n        return this.bounds.bottom();\n    }\n\n    @Override\n    public float scale() {\n        return 1;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public @Nullable ScreenRectangle bounds() {\n        return this.scissorArea != null ? this.scissorArea.intersection(this.bounds) : this.bounds;\n    }\n\n    public static class Renderer extends PictureInPictureRenderer<OwoItemElementRenderState> {\n\n        public Renderer(MultiBufferSource.BufferSource vertexConsumers) {\n            super(vertexConsumers);\n        }\n\n        @Override\n        public Class<OwoItemElementRenderState> getRenderStateClass() {\n            return OwoItemElementRenderState.class;\n        }\n\n        @Override\n        protected void renderToTexture(OwoItemElementRenderState state, PoseStack matrices) {\n            matrices.scale(state.bounds.width(), -state.bounds.height(), -Math.min(state.bounds.width(), state.bounds.height()));\n\n            var notSideLit = !state.item.usesBlockLight();\n            if (notSideLit) {\n                Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ITEMS_FLAT);\n            } else {\n                Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ITEMS_3D);\n            }\n\n            var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();\n            state.item.submit(matrices, dispatcher.getSubmitNodeStorage(), LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY, 0);\n            dispatcher.renderAllFeatures();\n        }\n\n        @Override\n        protected float getTranslateY(int height, int windowScaleFactor) {\n            return height / 2f;\n        }\n\n        @Override\n        protected String getTextureLabel() {\n            return \"owo-item\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/OwoSpecialGuiElementRenderers.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport io.wispforest.owo.braid.core.element.BraidBlockElement;\nimport io.wispforest.owo.braid.core.element.BraidEntityElement;\nimport io.wispforest.owo.braid.core.element.BraidItemElement;\nimport net.fabricmc.fabric.api.client.rendering.v1.SpecialGuiElementRegistry;\n\npublic class OwoSpecialGuiElementRenderers {\n    public static void init() {\n        SpecialGuiElementRegistry.register(ctx -> new CubeMapElementRenderState.Renderer(ctx.vertexConsumers()));\n        SpecialGuiElementRegistry.register(ctx -> new EntityElementRenderState.Renderer(ctx.vertexConsumers()));\n        SpecialGuiElementRegistry.register(ctx -> new BlockElementRenderState.Renderer(ctx.vertexConsumers()));\n        SpecialGuiElementRegistry.register(ctx -> new OwoItemElementRenderState.Renderer(ctx.vertexConsumers()));\n\n        SpecialGuiElementRegistry.register(ctx -> new BraidEntityElement.Renderer(ctx.vertexConsumers()));\n        SpecialGuiElementRegistry.register(ctx -> new BraidBlockElement.Renderer(ctx.vertexConsumers()));\n        SpecialGuiElementRegistry.register(ctx -> new BraidItemElement.Renderer(ctx.vertexConsumers()));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/renderstate/RingElementRenderState.java",
    "content": "package io.wispforest.owo.ui.renderstate;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport com.mojang.blaze3d.vertex.VertexConsumer;\nimport io.wispforest.owo.ui.core.Color;\nimport net.minecraft.client.gui.navigation.ScreenPosition;\nimport net.minecraft.client.gui.navigation.ScreenRectangle;\nimport net.minecraft.client.gui.render.TextureSetup;\nimport net.minecraft.client.gui.render.state.GuiElementRenderState;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\n\npublic record RingElementRenderState(\n    RenderPipeline pipeline,\n    Matrix3x2f pose,\n    ScreenRectangle scissorArea,\n    int centerX,\n    int centerY,\n    double angleFrom,\n    double angleTo,\n    int segments,\n    double innerRadius,\n    double outerRadius,\n    Color innerColor,\n    Color outerColor\n) implements GuiElementRenderState {\n    @Override\n    public void buildVertices(VertexConsumer vertices) {\n        double angleStep = Math.toRadians(this.angleTo - this.angleFrom) / this.segments;\n        int inColor = this.innerColor.argb();\n        int outColor = this.outerColor.argb();\n\n        for (int i = 0; i <= this.segments; i++) {\n            double theta = Math.toRadians(this.angleFrom) + i * angleStep;\n\n            vertices.addVertexWith2DPose(this.pose, (float) (this.centerX - Math.cos(theta) * this.outerRadius), (float) (this.centerY - Math.sin(theta) * this.outerRadius))\n                .setColor(outColor);\n            vertices.addVertexWith2DPose(this.pose, (float) (this.centerX - Math.cos(theta) * this.innerRadius), (float) (this.centerY - Math.sin(theta) * this.innerRadius))\n                .setColor(inColor);\n        }\n    }\n\n    @Override\n    public RenderPipeline pipeline() {\n        return this.pipeline;\n    }\n\n    @Override\n    public TextureSetup textureSetup() {\n        return TextureSetup.noTexture();\n    }\n\n    @Override\n    public @Nullable ScreenRectangle scissorArea() {\n        return this.scissorArea;\n    }\n\n    @Override\n    public ScreenRectangle bounds() {\n        var screenRect = new ScreenRectangle(\n            new ScreenPosition((int) (this.centerX - this.outerRadius), (int) (this.centerY - this.outerRadius)),\n            (int) Math.ceil(this.outerRadius * 2),\n            (int) Math.ceil(this.outerRadius * 2)\n        ).transformMaxBounds(this.pose);\n\n        return this.scissorArea != null ? this.scissorArea.intersection(screenRect) : screenRect;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/CommandOpenedScreen.java",
    "content": "package io.wispforest.owo.ui.util;\n\n/**\n * A marker interface for screens that are opened by client-sided commands\n * which prevents the chat screen from closing the newly opened screen\n */\npublic interface CommandOpenedScreen {}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/CursorAdapter.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport com.mojang.blaze3d.platform.Window;\nimport io.wispforest.owo.ui.core.CursorStyle;\nimport net.minecraft.client.Minecraft;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.EnumMap;\n\npublic class CursorAdapter {\n\n    protected static final CursorStyle[] ACTIVE_STYLES = {CursorStyle.POINTER, CursorStyle.TEXT, CursorStyle.HAND, CursorStyle.CROSSHAIR, CursorStyle.MOVE, CursorStyle.HORIZONTAL_RESIZE, CursorStyle.VERTICAL_RESIZE, CursorStyle.NWSE_RESIZE, CursorStyle.NESW_RESIZE, CursorStyle.NOT_ALLOWED};\n\n    protected final EnumMap<CursorStyle, Long> cursors = new EnumMap<>(CursorStyle.class);\n    protected final long windowHandle;\n\n    protected CursorStyle lastCursorStyle = CursorStyle.POINTER;\n    protected boolean disposed = false;\n\n    protected CursorAdapter(long windowHandle) {\n        this.windowHandle = windowHandle;\n        for (var style : ACTIVE_STYLES) {\n            var pointer = GLFW.glfwCreateStandardCursor(style.glfw);\n            if (pointer == 0) continue;\n\n            this.cursors.put(style, pointer);\n        }\n    }\n\n    public static CursorAdapter ofClientWindow() {\n        return new CursorAdapter(Minecraft.getInstance().getWindow().handle());\n    }\n\n    public static CursorAdapter ofWindow(Window window) {\n        return new CursorAdapter(window.handle());\n    }\n\n    public static CursorAdapter ofWindow(long windowHandle) {\n        return new CursorAdapter(windowHandle);\n    }\n\n    public void applyStyle(CursorStyle style) {\n        if (this.disposed || this.lastCursorStyle == style) return;\n\n        if (style == CursorStyle.NONE) {\n            GLFW.glfwSetCursor(this.windowHandle, 0);\n        } else {\n            GLFW.glfwSetCursor(this.windowHandle, this.cursors.getOrDefault(style, 0L));\n        }\n        this.lastCursorStyle = style;\n    }\n\n    public void dispose() {\n        if (this.disposed) return;\n\n        this.cursors.values().forEach(GLFW::glfwDestroyCursor);\n        this.disposed = true;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/Delta.java",
    "content": "package io.wispforest.owo.ui.util;\n\n/**\n * Trying to give this utility class a\n * sensible name makes me mald\n */\npublic final class Delta {\n\n    private Delta() {}\n\n    /**\n     * Compute an additive interpolator for smoothly approaching the\n     * target value given the current value and some interpolation\n     * delta\n     *\n     * @param current The current value\n     * @param target  The target value to approach\n     * @param delta   The interpolation delta - this is usually the frame delta,\n     *                optionally multiplied by some factor\n     * @return The computed interpolator, to be added to the current value\n     */\n    public static float compute(float current, float target, float delta) {\n        float diff = target - current;\n        delta = diff * delta;\n\n        return Math.abs(delta) > Math.abs(diff) ? diff : delta;\n    }\n\n    /**\n     * Compute an additive interpolator for smoothly approaching the\n     * target value given the current value and some interpolation\n     * delta\n     *\n     * @param current The current value\n     * @param target  The target value to approach\n     * @param delta   The interpolation delta - this is usually the frame delta,\n     *                optionally multiplied by some factor\n     * @return The computed interpolator, to be added to the current value\n     */\n    public static double compute(double current, double target, double delta) {\n        double diff = target - current;\n        delta = diff * delta;\n\n        return Math.abs(delta) > Math.abs(diff) ? diff : delta;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/DisposableScreen.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport net.minecraft.client.gui.screens.Screen;\n\n/**\n * Screens that wish to be notified when the players navigates back to\n * the game instead of to another screen may implement this interface\n * for a more reliable alternative to {@link Screen#removed()}\n */\npublic interface DisposableScreen {\n\n    /**\n     * Invoked when a best-effort algorithm has determined\n     * that the player is navigating to return to the game instead of opening\n     * another screen - ensured to be called too often than too rarely\n     */\n    void dispose();\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/FocusHandler.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport org.jetbrains.annotations.Contract;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.ArrayList;\n\npublic class FocusHandler {\n\n    protected final ParentUIComponent root;\n    @Nullable protected UIComponent focused = null;\n    @Nullable protected UIComponent.FocusSource lastFocusSource = null;\n\n    public FocusHandler(ParentUIComponent root) {\n        this.root = root;\n    }\n\n    public void updateClickFocus(double mouseX, double mouseY) {\n        var clicked = this.root.childAt((int) mouseX, (int) mouseY);\n        this.focus(clicked != null && clicked.canFocus(UIComponent.FocusSource.MOUSE_CLICK) ? clicked : null, UIComponent.FocusSource.MOUSE_CLICK);\n    }\n\n    @Contract(pure = true)\n    public @Nullable UIComponent focused() {\n        return this.focused;\n    }\n\n    public UIComponent.FocusSource lastFocusSource() {\n        return this.lastFocusSource;\n    }\n\n    public void cycle(boolean forwards) {\n        var allChildren = new ArrayList<UIComponent>();\n        this.root.collectDescendants(allChildren);\n\n        allChildren.removeIf(component -> !component.canFocus(UIComponent.FocusSource.KEYBOARD_CYCLE));\n        if (allChildren.isEmpty()) return;\n\n        int newIndex = this.focused == null\n                ? forwards ? 0 : allChildren.size() - 1\n                : (allChildren.indexOf(this.focused)) + (forwards ? 1 : -1);\n\n        if (newIndex >= allChildren.size()) newIndex -= allChildren.size();\n        if (newIndex < 0) newIndex += allChildren.size();\n\n        this.focus(allChildren.get(newIndex), UIComponent.FocusSource.KEYBOARD_CYCLE);\n    }\n\n    public void moveFocus(int keyCode) {\n        if (this.focused == null) return;\n\n        var allChildren = new ArrayList<UIComponent>();\n        this.root.collectDescendants(allChildren);\n\n        allChildren.removeIf(component -> !component.canFocus(UIComponent.FocusSource.KEYBOARD_CYCLE));\n        if (allChildren.isEmpty()) return;\n\n        var closest = this.focused;\n        switch (keyCode) {\n            case GLFW.GLFW_KEY_RIGHT -> {\n                int closestX = Integer.MAX_VALUE, closestY = Integer.MAX_VALUE;\n\n                for (var child : allChildren) {\n                    if (child == this.focused) continue;\n                    if (child.x() < this.focused.x() + this.focused.width() ||\n                            child.x() > closestX || Math.abs(child.y() - this.focused.y()) > closestY) continue;\n\n                    closest = child;\n                    closestX = child.x();\n                    closestY = Math.abs(child.y() - this.focused.y());\n                }\n            }\n            case GLFW.GLFW_KEY_LEFT -> {\n                int closestX = 0, closestY = Integer.MAX_VALUE;\n\n                for (var child : allChildren) {\n                    if (child == this.focused) continue;\n                    if (child.x() + child.width() > this.focused.x() ||\n                            child.x() + child.width() < closestX || Math.abs(child.y() - this.focused.y()) > closestY) continue;\n\n                    closest = child;\n                    closestX = child.x() + child.width();\n                    closestY = Math.abs(child.y() - this.focused.y());\n                }\n            }\n            case GLFW.GLFW_KEY_UP -> {\n                int closestX = Integer.MAX_VALUE, closestY = 0;\n\n                for (var child : allChildren) {\n                    if (child == this.focused) continue;\n                    if (child.y() + child.height() > this.focused.y() ||\n                            child.y() + child.height() < closestY || Math.abs(child.x() - this.focused.x()) > closestX) continue;\n\n                    closest = child;\n                    closestX = Math.abs(child.x() - this.focused.x());\n                    closestY = child.y() + child.height();\n                }\n            }\n            case GLFW.GLFW_KEY_DOWN -> {\n                int closestX = Integer.MAX_VALUE, closestY = Integer.MAX_VALUE;\n\n                for (var child : allChildren) {\n                    if (child == this.focused) continue;\n                    if (child.y() < this.focused.y() + this.focused.height() ||\n                            child.y() + child.height() > closestY || Math.abs(child.x() - this.focused.x()) > closestX) continue;\n\n                    closest = child;\n                    closestX = Math.abs(child.x() - this.focused.x());\n                    closestY = child.y() + child.height();\n                }\n            }\n        }\n\n        this.focus(closest, UIComponent.FocusSource.KEYBOARD_CYCLE);\n    }\n\n    public void focus(@Nullable UIComponent component, UIComponent.FocusSource source) {\n        if (this.focused != component) {\n            if (this.focused != null) {\n                this.focused.onFocusLost();\n            }\n\n            if ((this.focused = component) != null) {\n                this.focused.onFocusGained(source);\n                this.lastFocusSource = source;\n            } else {\n                this.lastFocusSource = null;\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/MatrixStackTransformer.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport net.minecraft.client.gui.GuiGraphics;\nimport org.joml.Matrix3x2f;\nimport org.joml.Matrix3x2fStack;\n\n/**\n * Helper interface implemented on top of the {@link GuiGraphics} to allow for easier matrix stack transformations\n */\npublic interface MatrixStackTransformer {\n\n    default MatrixStackTransformer translate(double x, double y) {\n        this.getMatrixStack().translate((float) x, (float) y);\n        return this;\n    }\n\n    default MatrixStackTransformer translate(float x, float y) {\n        this.getMatrixStack().translate(x, y);\n        return this;\n    }\n\n    default MatrixStackTransformer scale(float x, float y) {\n        this.getMatrixStack().scale(x, y);\n        return this;\n    }\n\n    default MatrixStackTransformer push() {\n        this.getMatrixStack().pushMatrix();\n        return this;\n    }\n\n    default MatrixStackTransformer pop() {\n        this.getMatrixStack().popMatrix();\n        return this;\n    }\n\n    default MatrixStackTransformer mul(Matrix3x2f matrix) {\n        this.getMatrixStack().mul(matrix);\n        return this;\n    }\n\n    default Matrix3x2fStack getMatrixStack(){\n        throw new IllegalStateException(\"getMatrices() method hasn't been override leading to exception!\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/MountingHelper.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport io.wispforest.owo.ui.core.Positioning;\nimport io.wispforest.owo.ui.core.Size;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\npublic class MountingHelper {\n\n    protected final ComponentSink sink;\n    protected final List<UIComponent> lateChildren;\n\n    protected MountingHelper(ComponentSink sink, List<UIComponent> children) {\n        this.sink = sink;\n        this.lateChildren = children;\n    }\n\n    public static void inflateWithExpand(List<UIComponent> children, Size childSpace, boolean vertical, int gap) {\n        var nonExpandChildren = new ArrayList<UIComponent>();\n\n        children.forEach(child -> {\n            if (!child.verticalSizing().get().isExpand() && !child.horizontalSizing().get().isExpand()) {\n                if(child.positioning().get().type == Positioning.Type.LAYOUT) {\n                    nonExpandChildren.add(child);\n                }\n\n                child.inflate(childSpace);\n            }\n        });\n\n        Size remainingSpace;\n        if (vertical) {\n            int height = childSpace.height();\n            for (var nonExpandChild : nonExpandChildren) {\n                height -= nonExpandChild.fullSize().height();\n            }\n\n            height -= gap * Math.max(children.size() - 1, 0);\n            remainingSpace = Size.of(childSpace.width(), Math.max(0, height));\n        } else {\n            int width = childSpace.width();\n            for (var nonExpandChild : nonExpandChildren) {\n                width -= nonExpandChild.fullSize().width();\n            }\n\n            width -= gap * Math.max(children.size() - 1, 0);\n            remainingSpace = Size.of(Math.max(0, width), childSpace.height());\n        }\n\n\n        children.forEach(child -> {\n            if (child.verticalSizing().get().isExpand() || child.horizontalSizing().get().isExpand()) {\n                child.inflate(remainingSpace);\n            }\n        });\n    }\n\n    public static MountingHelper mountEarly(ComponentSink sink, List<UIComponent> children, Consumer<UIComponent> layoutFunc) {\n        var lateChildren = new ArrayList<UIComponent>();\n\n        for (var child : children) {\n            if (!child.positioning().get().isRelative()) {\n                sink.accept(child, layoutFunc);\n            } else {\n                lateChildren.add(child);\n            }\n        }\n\n        return new MountingHelper(sink, lateChildren);\n    }\n\n    public void mountLate() {\n        for (var child : this.lateChildren) {\n            this.sink.accept(child, component -> {throw new IllegalStateException(\"A layout-positioned child was mounted late\");});\n        }\n        this.lateChildren.clear();\n    }\n\n    public interface ComponentSink {\n        void accept(@Nullable UIComponent child, Consumer<UIComponent> layoutFunc);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/NinePatchTexture.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport com.mojang.blaze3d.pipeline.RenderPipeline;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.StructEndec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.ui.core.Color;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.core.PositionedRectangle;\nimport io.wispforest.owo.ui.core.Size;\nimport net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener;\nimport net.minecraft.client.renderer.RenderPipelines;\nimport net.minecraft.resources.FileToIdConverter;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.server.packs.resources.ResourceManager;\nimport net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;\nimport net.minecraft.util.profiling.ProfilerFiller;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\npublic class NinePatchTexture {\n\n    private final Identifier texture;\n    private final int u, v;\n    private final PatchSizing patchSizing;\n    private final Size textureSize;\n    private final boolean repeat;\n\n    public NinePatchTexture(Identifier texture, int u, int v, PatchSizing patchSizing, Size textureSize, boolean repeat) {\n        this.texture = texture;\n        this.u = u;\n        this.v = v;\n        this.textureSize = textureSize;\n        this.patchSizing = patchSizing;\n        this.repeat = repeat;\n    }\n\n    public NinePatchTexture(Identifier texture, int u, int v, Size cornerPatchSize, Size centerPatchSize, Size textureSize, boolean repeat) {\n        this(texture, u, v, new PatchSizing(null, cornerPatchSize, centerPatchSize), textureSize, repeat);\n    }\n\n    public NinePatchTexture(Identifier texture, int u, int v, Size patchSize, Size textureSize, boolean repeat) {\n        this(texture, u, v, new PatchSizing(patchSize, null, null), textureSize, repeat);\n    }\n\n    private Size cornerPatchSize() {\n        return this.patchSizing.cornerPatchSize();\n    }\n\n    private Size centerPatchSize() {\n        return this.patchSizing.centerPatchSize();\n    }\n\n    public void draw(OwoUIGraphics context, PositionedRectangle rectangle) {\n        this.draw(context, rectangle, Color.WHITE);\n    }\n\n    public void draw(OwoUIGraphics context, PositionedRectangle rectangle, Color color) {\n        this.draw(context, rectangle.x(), rectangle.y(), rectangle.width(), rectangle.height(), color);\n    }\n\n    public void draw(OwoUIGraphics context, int x, int y, int width, int height) {\n        this.draw(context, x, y, width, height, Color.WHITE);\n    }\n\n    public void draw(OwoUIGraphics context, int x, int y, int width, int height, Color color) {\n        this.draw(context, RenderPipelines.GUI_TEXTURED, x, y, width, height, color);\n    }\n\n    public void draw(OwoUIGraphics context, RenderPipeline pipeline, int x, int y, int width, int height) {\n        this.draw(context, pipeline, x, y, width, height, Color.WHITE);\n    }\n\n    public void draw(OwoUIGraphics context, RenderPipeline pipeline, int x, int y, int width, int height, Color color) {\n        int rightEdge = this.cornerPatchSize().width() + this.centerPatchSize().width();\n        int bottomEdge = this.cornerPatchSize().height() + this.centerPatchSize().height();\n\n        context.blit(pipeline, this.texture, x, y, this.u, this.v, this.cornerPatchSize().width(), this.cornerPatchSize().height(), this.textureSize.width(), this.textureSize.height(), color.argb());\n        context.blit(pipeline, this.texture, x + width - this.cornerPatchSize().width(), y, this.u + rightEdge, this.v, this.cornerPatchSize().width(), this.cornerPatchSize().height(), this.textureSize.width(), this.textureSize.height(), color.argb());\n        context.blit(pipeline, this.texture, x, y + height - this.cornerPatchSize().height(), this.u, this.v + bottomEdge, this.cornerPatchSize().width(), this.cornerPatchSize().height(), this.textureSize.width(), this.textureSize.height(), color.argb());\n        context.blit(pipeline, this.texture, x + width - this.cornerPatchSize().width(), y + height - this.cornerPatchSize().height(), this.u + rightEdge, this.v + bottomEdge, this.cornerPatchSize().width(), this.cornerPatchSize().height(), this.textureSize.width(), this.textureSize.height(), color.argb());\n\n        if (this.repeat) {\n            this.drawRepeated(context, pipeline, x, y, width, height, color);\n        } else {\n            this.drawStretched(context, pipeline, x, y, width, height, color);\n        }\n    }\n\n    protected void drawStretched(OwoUIGraphics context, RenderPipeline pipeline, int x, int y, int width, int height, Color color) {\n        int doubleCornerHeight = this.cornerPatchSize().height() * 2;\n        int doubleCornerWidth = this.cornerPatchSize().width() * 2;\n\n        int rightEdge = this.cornerPatchSize().width() + this.centerPatchSize().width();\n        int bottomEdge = this.cornerPatchSize().height() + this.centerPatchSize().height();\n\n        if (width > doubleCornerWidth && height > doubleCornerHeight) {\n            context.blit(pipeline, this.texture, x + this.cornerPatchSize().width(), y + this.cornerPatchSize().height(),\n                this.u + this.cornerPatchSize().width(), this.v + this.cornerPatchSize().height(),\n                width - doubleCornerWidth, height - doubleCornerHeight,\n                this.centerPatchSize().width(), this.centerPatchSize().height(),\n                this.textureSize.width(), this.textureSize.height(), color.argb());\n        }\n\n        if (width > doubleCornerWidth) {\n            context.blit(pipeline, this.texture, x + this.cornerPatchSize().width(), y,\n                this.u + this.cornerPatchSize().width(), this.v,\n                width - doubleCornerWidth, this.cornerPatchSize().height(),\n                this.centerPatchSize().width(), this.cornerPatchSize().height(),\n                this.textureSize.width(), this.textureSize.height(), color.argb());\n            context.blit(pipeline, this.texture, x + this.cornerPatchSize().width(), y + height - this.cornerPatchSize().height(),\n                this.u + this.cornerPatchSize().width(), this.v + bottomEdge,\n                width - doubleCornerWidth, this.cornerPatchSize().height(),\n                this.centerPatchSize().width(), this.cornerPatchSize().height(),\n                this.textureSize.width(), this.textureSize.height(), color.argb());\n        }\n\n        if (height > doubleCornerHeight) {\n            context.blit(pipeline, this.texture, x, y + this.cornerPatchSize().height(),\n                this.u, this.v + this.cornerPatchSize().height(),\n                this.cornerPatchSize().width(), height - doubleCornerHeight,\n                this.cornerPatchSize().width(), this.centerPatchSize().height(),\n                this.textureSize.width(), this.textureSize.height(), color.argb());\n            context.blit(pipeline, this.texture, x + width - this.cornerPatchSize().width(), y + this.cornerPatchSize().height(),\n                this.u + rightEdge, this.v + this.cornerPatchSize().height(),\n                this.cornerPatchSize().width(), height - doubleCornerHeight,\n                this.cornerPatchSize().width(), this.centerPatchSize().height(),\n                this.textureSize.width(), this.textureSize.height(), color.argb());\n        }\n    }\n\n    protected void drawRepeated(OwoUIGraphics context, RenderPipeline pipeline, int x, int y, int width, int height, Color color) {\n        int doubleCornerHeight = this.cornerPatchSize().height() * 2;\n        int doubleCornerWidth = this.cornerPatchSize().width() * 2;\n\n        int rightEdge = this.cornerPatchSize().width() + this.centerPatchSize().width();\n        int bottomEdge = this.cornerPatchSize().height() + this.centerPatchSize().height();\n\n        if (width > doubleCornerWidth && height > doubleCornerHeight) {\n            int leftoverHeight = height - doubleCornerHeight;\n            while (leftoverHeight > 0) {\n                int drawHeight = Math.min(this.centerPatchSize().height(), leftoverHeight);\n\n                int leftoverWidth = width - doubleCornerWidth;\n                while (leftoverWidth > 0) {\n                    int drawWidth = Math.min(this.centerPatchSize().width(), leftoverWidth);\n                    context.blit(pipeline, this.texture,\n                        x + this.cornerPatchSize().width() + leftoverWidth - drawWidth, y + this.cornerPatchSize().height() + leftoverHeight - drawHeight,\n                        this.u + this.cornerPatchSize().width() + this.centerPatchSize().width() - drawWidth, this.v + this.cornerPatchSize().height() + this.centerPatchSize().height() - drawHeight,\n                        drawWidth, drawHeight,\n                        drawWidth, drawHeight,\n                        this.textureSize.width(), this.textureSize.height(), color.argb());\n\n                    leftoverWidth -= this.centerPatchSize().width();\n                }\n                leftoverHeight -= this.centerPatchSize().height();\n            }\n        }\n\n        if (width > doubleCornerWidth) {\n            int leftoverWidth = width - doubleCornerWidth;\n            while (leftoverWidth > 0) {\n                int drawWidth = Math.min(this.centerPatchSize().width(), leftoverWidth);\n\n                context.blit(pipeline, this.texture, x + this.cornerPatchSize().width() + leftoverWidth - drawWidth, y,\n                    this.u + this.cornerPatchSize().width() + this.centerPatchSize().width() - drawWidth, this.v,\n                    drawWidth, this.cornerPatchSize().height(),\n                    drawWidth, this.cornerPatchSize().height(),\n                    this.textureSize.width(), this.textureSize.height(), color.argb());\n                context.blit(pipeline, this.texture, x + this.cornerPatchSize().width() + leftoverWidth - drawWidth, y + height - this.cornerPatchSize().height(),\n                    this.u + this.cornerPatchSize().width() + this.centerPatchSize().width() - drawWidth, this.v + bottomEdge,\n                    drawWidth, this.cornerPatchSize().height(),\n                    drawWidth, this.cornerPatchSize().height(),\n                    this.textureSize.width(), this.textureSize.height(), color.argb());\n\n                leftoverWidth -= this.centerPatchSize().width();\n            }\n        }\n\n        if (height > doubleCornerHeight) {\n            int leftoverHeight = height - doubleCornerHeight;\n            while (leftoverHeight > 0) {\n                int drawHeight = Math.min(this.centerPatchSize().height(), leftoverHeight);\n                context.blit(pipeline, this.texture, x, y + this.cornerPatchSize().height() + leftoverHeight - drawHeight,\n                    this.u, this.v + this.cornerPatchSize().height() + this.centerPatchSize().height() - drawHeight,\n                    this.cornerPatchSize().width(), drawHeight,\n                    this.cornerPatchSize().width(), drawHeight,\n                    this.textureSize.width(), this.textureSize.height(), color.argb());\n                context.blit(pipeline, this.texture, x + width - this.cornerPatchSize().width(), y + this.cornerPatchSize().height() + leftoverHeight - drawHeight,\n                    this.u + rightEdge, this.v + this.cornerPatchSize().height() + this.centerPatchSize().height() - drawHeight,\n                    this.cornerPatchSize().width(), drawHeight,\n                    this.cornerPatchSize().width(), drawHeight,\n                    this.textureSize.width(), this.textureSize.height(), color.argb());\n\n                leftoverHeight -= this.centerPatchSize().height();\n            }\n        }\n    }\n\n    public static void draw(Identifier texture, OwoUIGraphics context, int x, int y, int width, int height) {\n        draw(texture, context, RenderPipelines.GUI_TEXTURED, x, y, width, height);\n    }\n\n    public static void draw(Identifier texture, OwoUIGraphics context, int x, int y, int width, int height, Color color) {\n        draw(texture, context, RenderPipelines.GUI_TEXTURED, x, y, width, height, color);\n    }\n\n    public static void draw(Identifier texture, OwoUIGraphics context, RenderPipeline pipeline, int x, int y, int width, int height) {\n        ifPresent(texture, ninePatchTexture -> ninePatchTexture.draw(context, pipeline, x, y, width, height));\n    }\n\n    public static void draw(Identifier texture, OwoUIGraphics context, RenderPipeline pipeline, int x, int y, int width, int height, Color color) {\n        ifPresent(texture, ninePatchTexture -> ninePatchTexture.draw(context, pipeline, x, y, width, height, color));\n    }\n\n    public static void draw(Identifier texture, OwoUIGraphics context, PositionedRectangle rectangle) {\n        ifPresent(texture, ninePatchTexture -> ninePatchTexture.draw(context, rectangle));\n    }\n\n    public static void draw(Identifier texture, OwoUIGraphics context, PositionedRectangle rectangle, Color color) {\n        ifPresent(texture, ninePatchTexture -> ninePatchTexture.draw(context, rectangle, color));\n    }\n\n    private static void ifPresent(Identifier texture, Consumer<NinePatchTexture> action) {\n        if (!MetadataLoader.LOADED_TEXTURES.containsKey(texture)) return;\n        action.accept(MetadataLoader.LOADED_TEXTURES.get(texture));\n    }\n\n    public static final Endec<NinePatchTexture> ENDEC = StructEndecBuilder.of(\n        MinecraftEndecs.IDENTIFIER.fieldOf(\"texture\", (texture) -> texture.texture),\n        Endec.INT.optionalFieldOf(\"u\", (texture) -> texture.u, 0),\n        Endec.INT.optionalFieldOf(\"v\", (texture) -> texture.v, 0),\n        PatchSizing.ENDEC.flatFieldOf((texture) -> texture.patchSizing),\n        StructEndecBuilder.of(\n            Endec.INT.fieldOf(\"texture_width\", Size::width),\n            Endec.INT.fieldOf(\"texture_height\", Size::height),\n            Size::of\n        ).flatFieldOf((texture) -> texture.textureSize),\n        Endec.BOOLEAN.fieldOf(\"repeat\", (texture) -> texture.repeat),\n        NinePatchTexture::new\n    );\n\n    public record PatchSizing(@Nullable Size patchSize, @Nullable Size cornerPatchSize, @Nullable Size centerPatchSize) {\n        public static final StructEndec<PatchSizing> ENDEC = StructEndecBuilder.of(\n            Size.ENDEC.nullableOf().optionalFieldOf(\"patch_size\", PatchSizing::patchSize, () -> null),\n            Size.ENDEC.nullableOf().optionalFieldOf(\"corner_patch_size\", PatchSizing::cornerPatchSize, () -> null),\n            Size.ENDEC.nullableOf().optionalFieldOf(\"center_patch_size\", PatchSizing::centerPatchSize, () -> null),\n            PatchSizing::new\n        );\n\n        public PatchSizing {\n            if (patchSize == null) {\n                if ((cornerPatchSize != null && centerPatchSize == null)) {\n                    throw new IllegalStateException(\"Missing center Patch Size while providing corner Patch Size!\");\n                } else if ((cornerPatchSize == null && centerPatchSize != null)) {\n                    throw new IllegalStateException(\"Missing corner Patch Size while providing center Patch Size!\");\n                } else if ((cornerPatchSize == null && centerPatchSize == null)) {\n                    throw new IllegalStateException(\"Missing base patch Size or patch size for both corner and center!\");\n                }\n            }\n        }\n\n        @NotNull\n        @Override\n        public Size cornerPatchSize() {\n            return (this.cornerPatchSize != null) ? this.cornerPatchSize : this.patchSize;\n        }\n\n        @NotNull\n        @Override\n        public Size centerPatchSize() {\n            return (this.centerPatchSize != null) ? this.centerPatchSize : this.patchSize;\n        }\n    }\n\n    public static class MetadataLoader extends SimpleJsonResourceReloadListener<NinePatchTexture> implements IdentifiableResourceReloadListener {\n\n        private static final Map<Identifier, NinePatchTexture> LOADED_TEXTURES = new HashMap<>();\n\n        public MetadataLoader() {\n            super(CodecUtils.toCodec(NinePatchTexture.ENDEC), FileToIdConverter.json(\"nine_patch_textures\"));\n        }\n\n        @Override\n        public Identifier getFabricId() {\n            return Owo.id(\"nine_patch_metadata\");\n        }\n\n        protected void apply(Map<Identifier, NinePatchTexture> prepared, ResourceManager manager, ProfilerFiller profiler) {\n            LOADED_TEXTURES.putAll(prepared);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/SpriteUtilInvoker.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport io.wispforest.owo.Owo;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.client.renderer.texture.TextureAtlasSprite;\n\nimport java.lang.invoke.MethodHandle;\nimport java.lang.invoke.MethodHandles;\nimport java.lang.invoke.MethodType;\n\npublic class SpriteUtilInvoker {\n    private static final MethodHandle MARK_SPRITE_ACTIVE = getMarkSpriteActive();\n\n    public static void markSpriteActive(TextureAtlasSprite sprite) {\n        try {\n            MARK_SPRITE_ACTIVE.invoke((TextureAtlasSprite) sprite);\n        } catch (Throwable e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static MethodHandle getMarkSpriteActive() {\n        if (FabricLoader.getInstance().isModLoaded(\"sodium\")) {\n            try {\n                Class<?> spriteUtil = Class.forName(\"me.jellysquid.mods.sodium.client.render.texture.SpriteUtil\");\n                var m = spriteUtil.getMethod(\"markSpriteActive\", TextureAtlasSprite.class);\n                m.setAccessible(true);\n                return MethodHandles.lookup().unreflect(m);\n            } catch (Exception e) {\n                Owo.LOGGER.error(\"Couldn't get SpriteUtil.markSpriteActive from Sodium\", e);\n            }\n        }\n\n        return MethodHandles.empty(MethodType.methodType(void.class, TextureAtlasSprite.class));\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/UIErrorToast.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ops.TextOps;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.owo.ui.parsing.UIModelLoader;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.Font;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.toasts.Toast;\nimport net.minecraft.client.gui.components.toasts.ToastManager;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.util.FormattedCharSequence;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Consumer;\n\n@ApiStatus.Internal\npublic class UIErrorToast implements Toast {\n\n    private final List<FormattedCharSequence> errorMessage;\n    private final Font textRenderer;\n    private final int width;\n\n    public UIErrorToast(Throwable error) {\n        this.textRenderer = Minecraft.getInstance().font;\n        var texts = this.initText(String.valueOf(error.getMessage()), (consumer) -> {\n            var stackTop = error.getStackTrace()[0];\n            var errorLocation = stackTop.getClassName().split(\"\\\\.\");\n\n            consumer.accept(Component.literal(\"Type: \").withStyle(ChatFormatting.RED)\n                    .append(Component.literal(error.getClass().getSimpleName()).withStyle(ChatFormatting.GRAY)));\n            consumer.accept(Component.literal(\"Thrown by: \").withStyle(ChatFormatting.RED)\n                    .append(Component.literal(errorLocation[errorLocation.length - 1] + \":\" + stackTop.getLineNumber()).withStyle(ChatFormatting.GRAY)));\n        });\n\n        this.width = Math.min(240, TextOps.width(textRenderer, texts) + 8);\n        this.errorMessage = this.wrap(texts);\n    }\n\n    public UIErrorToast(String message) {\n        this.textRenderer = Minecraft.getInstance().font;\n        var texts = this.initText(message, (consumer) -> {\n            consumer.accept(Component.literal(\"No context provided\").withStyle(ChatFormatting.GRAY));\n        });\n        this.width = Math.min(240, TextOps.width(textRenderer, texts) + 8);\n        this.errorMessage = this.wrap(texts);\n    }\n\n    public static void report(String message) {\n        logErrorsDuringInitialLoad();\n        Minecraft.getInstance().getToastManager().addToast(new UIErrorToast(message));\n    }\n\n    public static void report(Throwable error) {\n        logErrorsDuringInitialLoad();\n        Minecraft.getInstance().getToastManager().addToast(new UIErrorToast(error));\n    }\n\n    private static void logErrorsDuringInitialLoad() {\n        if (UIModelLoader.hasCompletedInitialLoad()) return;\n\n        var throwable = new Throwable();\n        Owo.LOGGER.error(\n                \"An owo-ui error has occurred during the initial resource reload (on thread {}). This is likely a bug caused by *some* other mod initializing an owo-config screen significantly too early - please report it at https://github.com/wisp-forest/owo-lib/issues\",\n                Thread.currentThread().getName(),\n                throwable\n        );\n    }\n\n    private Visibility visibility = Visibility.HIDE;\n\n    @Override\n    public void update(ToastManager manager, long time) {\n        this.visibility = time > 10000 ? Visibility.HIDE : Visibility.SHOW;\n    }\n\n    @Override\n    public Visibility getWantedVisibility() {\n        return this.visibility;\n    }\n\n    @Override\n    public void render(GuiGraphics context, Font textRenderer, long startTime) {\n        var owoContext = OwoUIGraphics.of(context);\n\n        owoContext.fill(0, 0, this.width(), this.height(), 0x77000000);\n        owoContext.drawRectOutline(0, 0, this.width(), this.height(), 0xA7FF0000);\n\n        int xOffset = this.width() / 2 - this.textRenderer.width(this.errorMessage.get(0)) / 2;\n        owoContext.drawString(this.textRenderer, this.errorMessage.get(0), 4 + xOffset, 4, 0xFFFFFFFF);\n\n        for (int i = 1; i < this.errorMessage.size(); i++) {\n            owoContext.drawString(this.textRenderer, this.errorMessage.get(i), 4, 4 + i * 11, 0xFFFFFFFF, false);\n        }\n    }\n\n    @Override\n    public int height() {\n        return 6 + this.errorMessage.size() * 11;\n    }\n\n    @Override\n    public int width() {\n        return this.width;\n    }\n\n    private List<Component> initText(String errorMessage, Consumer<Consumer<Component>> contextAppender) {\n        final var texts = new ArrayList<Component>();\n        texts.add(Component.literal(\"owo-ui error\").withStyle(ChatFormatting.RED));\n\n        texts.add(Component.literal(\" \"));\n        contextAppender.accept(texts::add);\n        texts.add(Component.literal(\" \"));\n\n        texts.add(Component.literal(errorMessage));\n\n        texts.add(Component.literal(\" \"));\n        texts.add(Component.literal(\"Check your log for details\").withStyle(ChatFormatting.GRAY));\n\n        return texts;\n    }\n\n    private List<FormattedCharSequence> wrap(List<Component> message) {\n        var list = new ArrayList<FormattedCharSequence>();\n        for (var text : message) list.addAll(this.textRenderer.split(text, this.width() - 8));\n        return list;\n    }\n\n    @Override\n    public Object getToken() {\n        return Type.VERY_TYPE;\n    }\n\n    enum Type {\n        VERY_TYPE\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/ui/util/UISounds.java",
    "content": "package io.wispforest.owo.ui.util;\n\nimport io.wispforest.owo.Owo;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.resources.sounds.SimpleSoundInstance;\nimport net.minecraft.sounds.SoundEvent;\nimport net.minecraft.sounds.SoundEvents;\n\npublic final class UISounds {\n\n    public static final SoundEvent UI_INTERACTION = SoundEvent.createVariableRangeEvent(Owo.id(\"ui.owo.interaction\"));\n\n    private UISounds() {}\n\n    @Environment(EnvType.CLIENT)\n    public static void play(SoundEvent event) {\n        Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(event, 1));\n    }\n\n    @Environment(EnvType.CLIENT)\n    public static void playButtonSound() {\n        play(SoundEvents.UI_BUTTON_CLICK.value());\n    }\n\n    @Environment(EnvType.CLIENT)\n    public static void playInteractionSound() {\n        play(UI_INTERACTION);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/DataExtensionUtil.java",
    "content": "package io.wispforest.owo.util;\n\nimport blue.endless.jankson.Jankson;\nimport blue.endless.jankson.JsonGrammar;\nimport blue.endless.jankson.api.SyntaxError;\nimport com.google.common.collect.MapMaker;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Set;\nimport java.util.function.Predicate;\n\nimport static java.util.Collections.newSetFromMap;\n\n@ApiStatus.Internal\npublic class DataExtensionUtil {\n    public static final ThreadLocal<Jankson> JANKSON = ThreadLocal.withInitial(() -> Jankson.builder().build());\n\n    public static final Set<String> JSON5_ENABLED_PACKS = newSetFromMap(new MapMaker().weakKeys().makeMap());\n\n    private DataExtensionUtil() {}\n\n    public static InputStream coerceJson(InputStream inputStream) {\n        try {\n            return new CoercedByteArrayInputStream(JANKSON\n                .get()\n                .load(inputStream)\n                .toJson(JsonGrammar.STRICT)\n                .getBytes(StandardCharsets.UTF_8)\n            );\n        } catch (IOException | SyntaxError e) {\n            throw new RuntimeException(\"Failed to convert JSON5 to JSON\", e);\n        }\n    }\n\n    public static class CoercedByteArrayInputStream extends ByteArrayInputStream {\n        public CoercedByteArrayInputStream(byte[] buf) {\n            super(buf);\n        }\n    }\n\n    public interface OptInIdentifierPredicate extends Predicate<Identifier> {\n        static OptInIdentifierPredicate of(Predicate<Identifier> delegate) {\n            return delegate instanceof OptInIdentifierPredicate optIn ? optIn : delegate::test;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/EventSource.java",
    "content": "package io.wispforest.owo.util;\n\npublic class EventSource<T> {\n\n    private final EventStream<T> stream;\n\n    protected EventSource(EventStream<T> stream) {\n        this.stream = stream;\n    }\n\n    public Subscription subscribe(T subscriber) {\n        this.stream.addSubscriber(subscriber);\n        return new Subscription(subscriber);\n    }\n\n    public class Subscription {\n        protected final T subscriber;\n\n        public Subscription(T subscriber) {\n            this.subscriber = subscriber;\n        }\n\n        public void cancel() {\n            EventSource.this.stream.removeSubscriber(this.subscriber);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/EventStream.java",
    "content": "package io.wispforest.owo.util;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Function;\n\npublic class EventStream<T> {\n\n    protected final Function<List<T>, T> sinkFactory;\n    protected final List<T> subscribers = new ArrayList<>();\n    protected final EventSource<T> source = new EventSource<>(this);\n    protected T sink;\n\n    public EventStream(Function<List<T>, T> sinkFactory) {\n        this.sinkFactory = sinkFactory;\n        this.regenerateSink();\n    }\n\n    public T sink() {\n        return this.sink;\n    }\n\n    public EventSource<T> source() {\n        return this.source;\n    }\n\n    protected void addSubscriber(T subscriber) {\n        this.subscribers.add(subscriber);\n        this.regenerateSink();\n    }\n\n    protected void removeSubscriber(T subscriber) {\n        this.subscribers.remove(subscriber);\n        this.regenerateSink();\n    }\n\n    protected void regenerateSink() {\n        this.sink = this.sinkFactory.apply(this.subscribers);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/ImplementedContainer.java",
    "content": "package io.wispforest.owo.util;\n\nimport net.minecraft.core.NonNullList;\nimport net.minecraft.world.Container;\nimport net.minecraft.world.ContainerHelper;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.ItemStack;\n\n/**\n * A simple {@code Inventory} implementation with only default methods + an item list getter.\n * <p>\n * Originally by Juuz\n */\npublic interface ImplementedContainer extends Container {\n\n    /**\n     * Retrieves the item list of this inventory.\n     * Must return the same instance every time it's called.\n     */\n    NonNullList<ItemStack> getItems();\n\n    /**\n     * Creates an inventory from the item list.\n     */\n    static ImplementedContainer of(NonNullList<ItemStack> items) {\n        return () -> items;\n    }\n\n    /**\n     * Creates a new inventory with the specified size.\n     */\n    static ImplementedContainer ofSize(int size) {\n        return of(NonNullList.withSize(size, ItemStack.EMPTY));\n    }\n\n    /**\n     * Returns the inventory size.\n     */\n    @Override\n    default int getContainerSize() {\n        return getItems().size();\n    }\n\n    /**\n     * Checks if the inventory is empty.\n     *\n     * @return true if this inventory has only empty stacks, false otherwise.\n     */\n    @Override\n    default boolean isEmpty() {\n        for (int i = 0; i < getContainerSize(); i++) {\n            ItemStack stack = getItem(i);\n            if (!stack.isEmpty()) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Retrieves the item in the slot.\n     */\n    @Override\n    default ItemStack getItem(int slot) {\n        return getItems().get(slot);\n    }\n\n    /**\n     * Removes items from an inventory slot.\n     *\n     * @param slot  The slot to remove from.\n     * @param count How many items to remove. If there are fewer items in the slot than what are requested,\n     *              takes all items in that slot.\n     */\n    @Override\n    default ItemStack removeItem(int slot, int count) {\n        ItemStack result = ContainerHelper.removeItem(getItems(), slot, count);\n        if (!result.isEmpty()) {\n            setChanged();\n        }\n        return result;\n    }\n\n    /**\n     * Removes all items from an inventory slot.\n     *\n     * @param slot The slot to remove from.\n     */\n    @Override\n    default ItemStack removeItemNoUpdate(int slot) {\n        return ContainerHelper.takeItem(getItems(), slot);\n    }\n\n    /**\n     * Replaces the current stack in an inventory slot with the provided stack.\n     *\n     * @param slot  The inventory slot of which to replace the itemstack.\n     * @param stack The replacing itemstack. If the stack is too big for\n     *              this inventory ({@link Container#getMaxStackSize()}),\n     *              it gets resized to this inventory's maximum amount.\n     */\n    @Override\n    default void setItem(int slot, ItemStack stack) {\n        getItems().set(slot, stack);\n        if (stack.getCount() > getMaxStackSize()) {\n            stack.setCount(getMaxStackSize());\n        }\n    }\n\n    /**\n     * Clears the inventory.\n     */\n    @Override\n    default void clearContent() {\n        getItems().clear();\n    }\n\n    /**\n     * Marks the state as dirty.\n     * Must be called after changes in the inventory, so that the game can properly save\n     * the inventory contents and notify neighboring blocks of inventory changes.\n     */\n    @Override\n    default void setChanged() {\n        // Override if you want behavior.\n    }\n\n    /**\n     * @return true if the player can use the inventory, false otherwise.\n     */\n    @Override\n    default boolean stillValid(Player player) {\n        return true;\n    }\n}"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/KawaiiUtil.java",
    "content": "package io.wispforest.owo.util;\n\npublic final class KawaiiUtil {\n\n    private KawaiiUtil() {}\n\n    private static final String[] kaomoji = \"\"\"\n            (* ^ ω ^)\n            (´ ∀ ` *)\n            ٩(◕‿◕｡)۶\n            ☆*:.｡.o(≧▽≦)o.｡.:*☆\n            (o^▽^o)\n            (⌒▽⌒)☆\n            <(￣︶￣)>\n            。.:☆*:･'(*⌒―⌒*)))\n            ヽ(・∀・)ﾉ\n            (´｡• ω •｡`)\n            (￣ω￣)\n            ｀;:゛;｀;･(°ε° )\n            (o･ω･o)\n            (＠＾◡＾)\n            ヽ(*・ω・)ﾉ\n            (o_ _)ﾉ彡☆\n            (^人^)\n            (o´▽`o)\n            (*´▽`*)\n            ｡ﾟ( ﾟ^∀^ﾟ)ﾟ｡\n            ( ´ ω ` )\n            (((o(*°▽°*)o)))\n            (≧◡≦)\n            (o´∀`o)\n            (´• ω •`)\n            (＾▽＾)\n            (⌒ω⌒)\n            ∑d(°∀°d)\n            ╰(▔∀▔)╯\n            (─‿‿─)\n            (*^‿^*)\n            ヽ(o^ ^o)ﾉ\n            (✯◡✯)\n            (◕‿◕)\n            (*≧ω≦*)\n            (☆▽☆)\n            (⌒‿⌒)\n            ＼(≧▽≦)／\n            ヽ(o＾▽＾o)ノ\n            ☆ ～('▽^人)\n            (*°▽°*)\n            ٩(｡•́‿•̀｡)۶\n            (✧ω✧)\n            ヽ(*⌒▽⌒*)ﾉ\n            (´｡• ᵕ •｡`)\n            ( ´ ▽ ` )\n            (￣▽￣)\n            ╰(*´︶`*)╯\n            ヽ(>∀<☆)ノ\n            o(≧▽≦)o\n            (☆ω☆)\n            (っ˘ω˘ς )\n            ＼(￣▽￣)／\n            (*¯︶¯*)\n            ＼(＾▽＾)／\n            ٩(◕‿◕)۶\n            (o˘◡˘o)\n            \\\\(★ω★)/\n            \\\\(^ヮ^)/\n            (〃＾▽＾〃)\n            (╯✧▽✧)╯\n            o(>ω<)o\n            o( ❛ᴗ❛ )o\n            ｡ﾟ(TヮT)ﾟ｡\n            ( ‾́ ◡ ‾́ )\n            (ﾉ´ヮ`)ﾉ*: ･ﾟ\n            (b ᵔ▽ᵔ)b\n            (๑˃ᴗ˂)ﻭ\n            (๑˘︶˘๑)\n            ( ˙꒳˙ )\n            (*꒦ິ꒳꒦ີ)\n            °˖✧◝(⁰▿⁰)◜✧˖°\n            (´･ᴗ･ ` )\n            (ﾉ◕ヮ◕)ﾉ*:･ﾟ✧\n            („• ֊ •„)\n            (.❛ ᴗ ❛.)\n            (⁀ᗢ⁀)\n            (￢‿￢ )\n            (¬‿¬ )\n            (*￣▽￣)b\n            ( ˙▿˙ )\n            (¯▿¯)\n            ( ◕▿◕ )\n            ＼(٥⁀▽⁀ )／\n            („• ᴗ •„)\n            (ᵔ◡ᵔ)\n            ( ´ ▿ ` )\n            (づ￣ ³￣)づ\n            (つ≧▽≦)つ\n            (つ✧ω✧)つ\n            (づ ◕‿◕ )づ\n            (⊃｡•́‿•̀｡)⊃\n            (つ . •́ _ʖ •̀ .)つ\n            (っಠ‿ಠ)っ\n            (づ◡﹏◡)づ\n            ⊂(´• ω •`⊂)\n            ⊂(･ω･*⊂)\n            ⊂(￣▽￣)⊃\n            ⊂( ´ ▽ ` )⊃\n            ( ~*-*)~\n            (^_~)\n            ( ﾟｏ⌒)\n            (^_-)≡☆\n            (^ω~)\n            (>ω^)\n            (~人^)\n            (^_-)\n            ( -_・)\n            (^_<)〜☆\n            (^人<)〜☆\n            ☆⌒(≧▽° )\n            ☆⌒(ゝ。∂)\n            (^_<)\n            (^_−)☆\n            (･ω<)☆\n            (^.~)☆\n            (^.~)\n            (ﾉ´ з `)ノ\n            (♡μ_μ)\n            (*^^*)♡\n            ☆⌒ヽ(*'､^*)chu\n            (♡-_-♡)\n            (￣ε￣＠)\n            ヽ(♡‿♡)ノ\n            ( ´ ∀ `)ノ～ ♡\n            (─‿‿─)♡\n            (´｡• ᵕ •｡`) ♡\n            (*♡∀♡)\n            (｡・//ε//・｡)\n            (´ ω `♡)\n            ♡( ◡‿◡ )\n            (◕‿◕)♡\n            (/▽＼*)｡o○♡\n            (ღ˘⌣˘ღ)\n            (♡°▽°♡)\n            ♡(｡- ω -)\n            ♡ ～('▽^人)\n            (´• ω •`) ♡\n            (´ ε ` )♡\n            (´｡• ω •｡`) ♡\n            ( ´ ▽ ` ).｡ｏ♡\n            ╰(*´︶`*)╯♡\n            (*˘︶˘*).｡.:*♡\n            (♡˙︶˙♡)\n            ♡＼(￣▽￣)／♡\n            (≧◡≦) ♡\n            (⌒▽⌒)♡\n            (*¯ ³¯*)♡\n            (っ˘з(˘⌣˘ ) ♡\n            ♡ (˘▽˘>ԅ( ˘⌣˘)\n            ( ˘⌣˘)♡(˘⌣˘ )\n            (/^-^(^ ^*)/ ♡\n            ٩(♡ε♡)۶\n            σ(≧ε≦σ) ♡\n            ♡ (⇀ 3 ↼)\n            ♡ (￣З￣)\n            (´♡‿♡`)\n            (°◡°♡)\n            Σ>―(〃°ω°〃)♡→\n            (´,,•ω•,,)♡\n            (´꒳`)♡\n            \"\"\".split(\"\\n\");\n\n    /**\n     * Prepends a randomly chosen Kaomoji to the given String\n     *\n     * @param string The string the process\n     * @return {@code string} with a random Kaomoji prepended\n     */\n    public static String uwuify(String string) {\n        return string + \" \" + uwuGen();\n    }\n\n    /**\n     * @return A random Kaomoji\n     */\n    public static String uwuGen() {\n        return kaomoji[(int) (Math.random() * (kaomoji.length - 1))];\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/Maldenhagen.java",
    "content": "package io.wispforest.owo.util;\n\nimport net.minecraft.world.level.block.Block;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * A simple utility class for making ore blocks update after they are generated.\n * This is especially useful for ores that are supposed to glow, as with the normal\n * ore feature they won't do that since lighting is never calculated for them\n */\npublic final class Maldenhagen {\n\n    private Maldenhagen() {}\n\n    private static final Set<Block> COPIUM_INJECTED = new HashSet<>();\n\n    /**\n     * Marks a block for update after generation\n     *\n     * @param block The block to update\n     */\n    public static void injectCopium(Block block) {\n        COPIUM_INJECTED.add(block);\n    }\n\n    /**\n     * @param block The block to test\n     * @return {@code true} if the block should update after generation\n     */\n    public static boolean isOnCopium(Block block) {\n        return COPIUM_INJECTED.contains(block);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/NumberReflection.java",
    "content": "package io.wispforest.owo.util;\n\nimport org.jetbrains.annotations.ApiStatus;\n\n/**\n * Slightly concerning and experimental helpers for working\n * with the reflective {@link Class} objects of the\n * number primitives and their wrappers\n */\n@ApiStatus.Experimental\npublic final class NumberReflection {\n\n    private NumberReflection() {}\n\n    /**\n     * Determines whether the given class represents a number type\n     *\n     * @param clazz The class to test\n     * @return {@code true} if {@code clazz} is either a primitive\n     * number type or one the respective wrappers\n     */\n    public static boolean isNumberType(Class<?> clazz) {\n        return (clazz.isPrimitive() && clazz != boolean.class && clazz != char.class)\n                || clazz == Byte.class\n                || clazz == Short.class\n                || clazz == Integer.class\n                || clazz == Long.class\n                || clazz == Double.class\n                || clazz == Float.class;\n    }\n\n    /**\n     * Determines whether the given class represents\n     * a floating point type\n     *\n     * @param clazz The class to test\n     * @return {@code true} if {@code clazz} is either a primitive floating point type\n     * or {@link Float} or {@link Double}\n     */\n    public static boolean isFloatingPointType(Class<?> clazz) {\n        return clazz == Float.class || clazz == float.class || clazz == Double.class || clazz == double.class;\n    }\n\n    /**\n     * Tries to convert the given number to {@code targetClass}\n     * by calling the corresponding {@code Number.<type>Value()} method\n     *\n     * @param in          The number to convert\n     * @param targetClass The target class, must be something which satisfies {@link #isNumberType(Class)}\n     * @return The input number, converted to the target type\n     * @throws IllegalArgumentException if either {@code targetClass} does not satisfy {@link #isNumberType(Class)}\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Number> T convert(Number in, Class<T> targetClass) {\n        if (!isNumberType(targetClass)) throw new IllegalArgumentException(\"Cannot convert to non-number target class\");\n\n        if (targetClass == Float.class || targetClass == float.class) {\n            return (T) (Float) in.floatValue();\n        } else if (targetClass == Double.class || targetClass == double.class) {\n            return (T) (Double) in.doubleValue();\n        } else if (targetClass == Byte.class || targetClass == byte.class) {\n            return (T) (Byte) in.byteValue();\n        } else if (targetClass == Short.class || targetClass == short.class) {\n            return (T) (Short) in.shortValue();\n        } else if (targetClass == Integer.class || targetClass == int.class) {\n            return (T) (Integer) in.intValue();\n        } else if (targetClass == Long.class || targetClass == long.class) {\n            return (T) (Long) in.longValue();\n        } else {\n            throw new IllegalStateException(\"Target class does not correspond to a supported number type - this should be unreachable\");\n        }\n    }\n\n    /**\n     * Tries to determine the maximum value supported by the number\n     * type which {@code numberType} represents\n     *\n     * @param numberType The target number type, must be something which satisfies {@link #isNumberType(Class)}\n     * @return The maximum value of the given number type\n     * @throws IllegalArgumentException if either {@code targetClass} does not satisfy {@link #isNumberType(Class)}\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Number> T maxValue(Class<T> numberType) {\n        if (!isNumberType(numberType)) throw new IllegalArgumentException(\"Cannot get maximum value of non-number class\");\n\n        if (numberType == Float.class || numberType == float.class) {\n            return (T) (Float) Float.MAX_VALUE;\n        } else if (numberType == Double.class || numberType == double.class) {\n            return (T) (Double) Double.MAX_VALUE;\n        } else if (numberType == Byte.class || numberType == byte.class) {\n            return (T) (Byte) Byte.MAX_VALUE;\n        } else if (numberType == Short.class || numberType == short.class) {\n            return (T) (Short) Short.MAX_VALUE;\n        } else if (numberType == Integer.class || numberType == int.class) {\n            return (T) (Integer) Integer.MAX_VALUE;\n        } else if (numberType == Long.class || numberType == long.class) {\n            return (T) (Long) Long.MAX_VALUE;\n        } else {\n            throw new IllegalStateException(\"Target class does not correspond to a supported number type - this should be unreachable\");\n        }\n    }\n\n    /**\n     * Tries to determine the minimum value supported by the number\n     * type which {@code numberType} represents\n     *\n     * @param numberType The target number type, must be something which satisfies {@link #isNumberType(Class)}\n     * @return The minimum value of the given number type\n     * @throws IllegalArgumentException if either {@code targetClass} does not satisfy {@link #isNumberType(Class)}\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Number> T minValue(Class<T> numberType) {\n        if (!isNumberType(numberType)) throw new IllegalArgumentException(\"Cannot get minimum value of non-number class\");\n\n        if (numberType == Float.class || numberType == float.class) {\n            return (T) (Float) (-Float.MAX_VALUE);\n        } else if (numberType == Double.class || numberType == double.class) {\n            return (T) (Double) (-Double.MAX_VALUE);\n        } else if (numberType == Byte.class || numberType == byte.class) {\n            return (T) (Byte) Byte.MIN_VALUE;\n        } else if (numberType == Short.class || numberType == short.class) {\n            return (T) (Short) Short.MIN_VALUE;\n        } else if (numberType == Integer.class || numberType == int.class) {\n            return (T) (Integer) Integer.MIN_VALUE;\n        } else if (numberType == Long.class || numberType == long.class) {\n            return (T) (Long) Long.MIN_VALUE;\n        } else {\n            throw new IllegalStateException(\"Target class does not correspond to a supported number type - this should be unreachable\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/Observable.java",
    "content": "package io.wispforest.owo.util;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.Consumer;\n\n/**\n * A container which allows observing changes to its value.\n * Every time the value is <i>changed</i>, i.e.\n * {@code Objects.equals(value, newValue)} evaluates to {@code false},\n * all observers added via {@link #observe(Consumer)} will be notified\n * and passed the new value\n *\n * @param <T> The type of object this observable holds\n * @see #observeAll(Runnable, Observable[])\n */\npublic class Observable<T> {\n\n    protected T value;\n    protected final List<Consumer<T>> observers;\n\n    protected Observable(T initial) {\n        this.value = initial;\n        this.observers = new ArrayList<>();\n    }\n\n    /**\n     * Creates a new observable container with\n     * the given initial value\n     */\n    public static <T> Observable<T> of(T initial) {\n        return new Observable<>(initial);\n    }\n\n    /**\n     * Notify the given observer whenever <i>any</i> of the given observables\n     * are updated. Context-less version {@link #observeAll(Consumer, Observable[])} which\n     * allows observing multiple observables of different types\n     *\n     * @param observer    The observer to notify\n     * @param observables The list of observable to observe\n     */\n    public static void observeAll(Runnable observer, Observable<?>... observables) {\n        for (var observable : observables) {\n            observable.observe(o -> observer.run());\n        }\n    }\n\n    /**\n     * Notify the given observer whenever <i>any</i> of the given observables\n     * are updated\n     *\n     * @param observer    The observer to notify\n     * @param observables The list of observable to observe\n     */\n    @SafeVarargs\n    public static <T> void observeAll(Consumer<T> observer, Observable<T>... observables) {\n        for (var observable : observables) {\n            observable.observe(observer);\n        }\n    }\n\n    /**\n     * @return The current value stored in this container\n     */\n    public T get() {\n        return this.value;\n    }\n\n    /**\n     * Change the value stored in this container to {@code newValue}.\n     * Observers will only be notified if {@code Objects.equals(value, newValue)}\n     * evaluates to {@code false}\n     *\n     * @param newValue The new value to store\n     */\n    public void set(T newValue) {\n        var oldValue = this.value;\n        this.value = newValue;\n\n        if (!Objects.equals(this.value, oldValue)) {\n            this.notifyObservers(newValue);\n        }\n    }\n\n    /**\n     * Add an observer function to be run every time\n     * the value stored in this container changes\n     */\n    public void observe(Consumer<T> observer) {\n        this.observers.add(observer);\n    }\n\n    protected void notifyObservers(T value) {\n        for (var observer : this.observers) {\n            observer.accept(value);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/OwoFreezer.java",
    "content": "package io.wispforest.owo.util;\n\nimport io.wispforest.owo.Owo;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A simple utility for freezing services after mod initialization.\n */\npublic final class OwoFreezer {\n    private static final List<Runnable> FREEZE_CALLBACKS = new ArrayList<>();\n    private static boolean IS_FROZEN = false;\n    private static String FREEZER_CLASS = null;\n\n    private OwoFreezer() {}\n\n    /**\n     * Registers an on freeze callback. The callback will be called when services are frozen\n     *\n     * @param callback the callback to register\n     */\n    public static void registerFreezeCallback(Runnable callback) {\n        FREEZE_CALLBACKS.add(callback);\n    }\n\n    /**\n     * @return {@code true} if services are frozen\n     */\n    public static boolean isFrozen() {\n        return IS_FROZEN;\n    }\n\n    /**\n     * Shorthand for checking if services aren't frozen, and throwing if not.\n     *\n     * @param pluralName the plural of the service being registered (e.g. \"Network channels\")\n     * @throws ServicesFrozenException if services are frozen\n     */\n    public static void checkRegister(String pluralName) {\n        if (OwoFreezer.isFrozen())\n            throw new ServicesFrozenException(pluralName + \" may only be registered during mod initialization\");\n    }\n\n    @ApiStatus.Internal\n    public static void freeze() {\n        if (IS_FROZEN) {\n            throw new ServicesFrozenException(ReflectionUtils.getCallingClassName(2) + \" tried to freeze services after they were already frozen by \" + FREEZER_CLASS);\n        }\n\n        IS_FROZEN = true;\n        FREEZER_CLASS = ReflectionUtils.getCallingClassName(2);\n\n        for (Runnable callback : FREEZE_CALLBACKS) {\n            callback.run();\n        }\n\n        if (!Owo.DEBUG) return;\n        Owo.LOGGER.info(\"Services frozen by '\" + FREEZER_CLASS + \"'\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/RecipeRemainderStorage.java",
    "content": "package io.wispforest.owo.util;\n\nimport net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport org.jetbrains.annotations.ApiStatus;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@ApiStatus.Internal\npublic final class RecipeRemainderStorage {\n\n    private RecipeRemainderStorage() {}\n\n    private static final Map<Identifier, Map<Item, ItemStack>> REMAINDERS = new HashMap<>();\n\n    public static void store(Identifier recipe, Map<Item, ItemStack> remainders) {\n        REMAINDERS.put(recipe, remainders);\n    }\n\n    public static boolean has(Identifier recipe) {\n        return REMAINDERS.containsKey(recipe);\n    }\n\n    public static Map<Item, ItemStack> get(Identifier recipe) {\n        return REMAINDERS.get(recipe);\n    }\n\n    static {\n        ServerLifecycleEvents.START_DATA_PACK_RELOAD.register((server, resourceManager) -> REMAINDERS.clear());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/ReflectionUtils.java",
    "content": "package io.wispforest.owo.util;\n\nimport io.wispforest.owo.registration.annotations.AssignedName;\nimport io.wispforest.owo.registration.annotations.IterationIgnored;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.lang.reflect.*;\nimport java.util.Locale;\nimport java.util.function.Consumer;\nimport java.util.function.Function;\n\n@ApiStatus.Experimental\npublic final class ReflectionUtils {\n\n    private ReflectionUtils() {}\n\n    /**\n     * Tries to instantiate the given class with a zero-args constructor call,\n     * throws a {@link RuntimeException} if it fails\n     *\n     * @param clazz The class to instantiate\n     * @param <C>   The type of object that results\n     * @return The created instance of <b>C</b>\n     */\n    public static <C> C tryInstantiateWithNoArgs(Class<C> clazz) {\n        try {\n            return clazz.getConstructor().newInstance();\n        } catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {\n            throw new RuntimeException((e instanceof NoSuchMethodException ? \"No zero-args constructor defined on class \" : \"Could not instantiate class \") + clazz, e);\n        }\n    }\n\n    /**\n     * Calls the {@link Constructor#newInstance(Object...)} method and\n     * wraps the exception in a {@link RuntimeException}, thus making it unchecked.\n     * <br>\n     * <b>Use this when you would otherwise rethrow</b>\n     *\n     * @param constructor The constructor to call\n     * @param args        The arguments to pass the constructor\n     * @param <C>         The type of object to create\n     * @return The created object\n     */\n    public static <C> C instantiate(Constructor<C> constructor, Object... args) {\n        try {\n            return constructor.newInstance(args);\n        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {\n            throw new RuntimeException(\"Wrapped object creation failure, look below for reason\", e);\n        }\n    }\n\n    /**\n     * Tries to obtain the public zero-args constructor of the given class.\n     * <b>Use this when no constructor constitutes an error condition or\n     * you previously checked for its existence with {@link #requireZeroArgsConstructor(Class, Function)}</b>\n     *\n     * @param clazz The class to get the constructor from\n     * @param <C>   The type of object the constructor will create\n     * @return The public zero-args constructor of the given class\n     */\n    public static <C> Constructor<C> getNoArgsConstructor(Class<C> clazz) {\n        try {\n            return clazz.getConstructor();\n        } catch (NoSuchMethodException e) {\n            throw new IllegalStateException(\"Class \" + clazz.getName() + \" does not declare a zero-args constructor\", e);\n        }\n    }\n\n    /**\n     * Iterates all accessible static fields of the given class and\n     * calls the field consumer on each applicable one\n     *\n     * @param clazz           The target class\n     * @param targetFieldType The field type match\n     * @param fieldConsumer   The function to apply to each field, supplied\n     *                        with the field's value and ID\n     * @param <C>             The type of {@code clazz}\n     * @param <F>             The type of field to match\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <C, F> void iterateAccessibleStaticFields(Class<C> clazz, Class<F> targetFieldType, FieldConsumer<F> fieldConsumer) {\n        for (var field : clazz.getDeclaredFields()) {\n            if (!Modifier.isStatic(field.getModifiers())) continue;\n\n            F value;\n            try {\n                value = (F) field.get(null);\n            } catch (IllegalAccessException e) {\n                continue;\n            }\n\n            if (value == null || !targetFieldType.isAssignableFrom(value.getClass())) continue;\n            if (field.isAnnotationPresent(IterationIgnored.class)) continue;\n\n            fieldConsumer.accept(value, getFieldName(field), field);\n        }\n    }\n\n    /**\n     * Returns the name of field in all lowercase, or\n     * the name defined by an {@link AssignedName} annotation\n     *\n     * @param field The field to check\n     * @return the properly formatted field name\n     */\n    public static String getFieldName(Field field) {\n        var fieldId = field.getName().toLowerCase(Locale.ROOT);\n        if (field.isAnnotationPresent(AssignedName.class)) fieldId = field.getAnnotation(AssignedName.class).value();\n        return fieldId;\n    }\n\n    /**\n     * Executes the given consumer on all subclasses that match {@code targetType}\n     *\n     * @param parent     The parent class\n     * @param targetType The subclass type to match\n     * @param action     The action to execute on each subclass\n     */\n    public static void forApplicableSubclasses(Class<?> parent, Class<?> targetType, Consumer<Class<?>> action) {\n        for (var subclass : parent.getDeclaredClasses()) {\n            if (!targetType.isAssignableFrom(subclass)) continue;\n            action.accept(subclass);\n        }\n    }\n\n    /**\n     * Verifies that the given class provides a public zero-args constructor.\n     * Throws an exception with a caller-controlled message if the constructor\n     * doesn't exist\n     *\n     * @param clazz           The class to check the existence of a zero-args constructor for\n     * @param reasonFormatter The error message to throw, gets the class name passed\n     */\n    public static void requireZeroArgsConstructor(Class<?> clazz, Function<String, String> reasonFormatter) {\n        boolean found = false;\n        for (var constructor : clazz.getConstructors()) {\n            if (constructor.getParameterCount() != 0) continue;\n            found = true;\n            break;\n        }\n\n        if (!found) throw new IllegalStateException(reasonFormatter.apply(clazz.getName()));\n    }\n\n    /**\n     * Tries to acquire the name of the calling class,\n     * {@code depth} frames up the call stack\n     *\n     * @param depth How many frames upwards to walk the call stack\n     * @return The name of the class at {@code depth} in the call stack or\n     * {@code <unknown>} if the class name was not found\n     */\n    public static String getCallingClassName(int depth) {\n        return StackWalker.getInstance().walk(s -> s\n                .skip(depth)\n                .map(StackWalker.StackFrame::getClassName)\n                .findFirst()).orElse(\"<unknown>\");\n    }\n\n    /**\n     * Determines the n-th type argument of the given type. If {@code type}\n     * is not a parameterized type, {@code null} is returned\n     *\n     * @param type  The type to query\n     * @param index The index of the type argument the retrieve\n     * @return The n-th type argument of {@code type} or {@code null} if {@code index}\n     * is out of bounds or the type argument is not a {@link Class}\n     */\n    public static @Nullable Class<?> getTypeArgument(Type type, int index) {\n        if (!(type instanceof ParameterizedType parameterizedType)) return null;\n\n        var typeArgs = parameterizedType.getActualTypeArguments();\n        if (index > typeArgs.length - 1) return null;\n\n        var typeArgument = typeArgs[index];\n        if (!(typeArgument instanceof Class<?> typeClass)) return null;\n\n        return typeClass;\n    }\n\n    @FunctionalInterface\n    public interface FieldConsumer<F> {\n        void accept(F value, String name, Field field);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/Scary.java",
    "content": "package io.wispforest.owo.util;\n\n/**\n * Annotations used to indicate that a given whatever is design in some manor that may or may not\n * cause you pain. Combined it might scare your skeleton out of your body having it run down the block.\n */\npublic @interface Scary {\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/ServicesFrozenException.java",
    "content": "package io.wispforest.owo.util;\n\npublic class ServicesFrozenException extends IllegalStateException {\n    public ServicesFrozenException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/StackTraceSupplier.java",
    "content": "package io.wispforest.owo.util;\n\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.Supplier;\n\npublic final class StackTraceSupplier implements Supplier<String> {\n    private final Throwable throwable;\n    private final @Nullable Supplier<String> message;\n\n    private StackTraceSupplier(Throwable throwable, @Nullable Supplier<String> message) {\n        this.throwable = throwable;\n        this.message = message;\n    }\n\n    public static StackTraceSupplier of(Throwable throwable) {\n        return new StackTraceSupplier(throwable, null);\n    }\n\n    public static StackTraceSupplier of(Throwable throwable, Supplier<String> supplier) {\n        return new StackTraceSupplier(throwable, supplier);\n    }\n\n    public static StackTraceSupplier of(String message) {\n        var error = new IllegalStateException(message)\n            .initCause(null);\n\n        return new StackTraceSupplier(error, null);\n    }\n\n    @Override\n    public String get() {\n        return message != null ? message.get() : throwable.getMessage();\n    }\n\n    public StackTraceElement[] getFullStackTrace() {\n        var innerThrowable = throwable();\n        while (innerThrowable.getCause() != null) {\n            innerThrowable = innerThrowable.getCause();\n\n            // Prevent possible infinite loops where the cause is itself the cause as it is not setup or chain of exceptions\n            if (innerThrowable == throwable()) break;\n        }\n        return innerThrowable.getStackTrace();\n    }\n\n    public Throwable throwable() {\n        return throwable;\n    }\n\n    @Override\n    public String toString() {\n        return \"StackTraceSupplier[\" +\n                \"throwable=\" + throwable + \", \" +\n                \"message=\" + message + ']';\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/TagInjector.java",
    "content": "package io.wispforest.owo.util;\n\nimport com.google.common.collect.ForwardingMap;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.tags.TagEntry;\nimport org.jetbrains.annotations.ApiStatus;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.*;\nimport java.util.function.Function;\n\n/**\n * A simple utility for inserting values into Tags at runtime\n */\npublic final class TagInjector {\n\n    @ApiStatus.Internal\n    public static final HashMap<TagLocation, Set<TagEntry>> ADDITIONS = new HashMap<>();\n\n    private static final Map<TagLocation, Set<TagEntry>> ADDITIONS_VIEW = new ForwardingMap<>() {\n        @Override\n        protected @NotNull Map<TagLocation, Set<TagEntry>> delegate() {\n            return Collections.unmodifiableMap(ADDITIONS);\n        }\n\n        @Override\n        public Set<TagEntry> get(@Nullable Object key) {\n            return Collections.unmodifiableSet(this.delegate().get(key));\n        }\n    };\n\n    private TagInjector() {}\n\n    /**\n     * @return A view of all planned tag injections\n     */\n    public static Map<TagLocation, Set<TagEntry>> getInjections() {\n        return ADDITIONS_VIEW;\n    }\n\n    /**\n     * Inject the given identifiers into the given tag\n     * <p>\n     * If any of the identifiers don't correspond to an entry in the\n     * given registry, you <i>will</i> break the tag.\n     * If the tag does not exist, it will be created.\n     *\n     * @param registry   The registry for which the injected tags should apply\n     * @param tag        The tag to insert into, this could contain all kinds of values\n     * @param entryMaker The function to use for creating tag entries from the given identifiers\n     * @param values     The values to insert\n     */\n    public static void injectRaw(Registry<?> registry, Identifier tag, Function<Identifier, TagEntry> entryMaker, Collection<Identifier> values) {\n        ADDITIONS.computeIfAbsent(new TagLocation(Registries.tagsDirPath(registry.key()), tag), identifier -> new HashSet<>())\n                .addAll(values.stream().map(entryMaker).toList());\n    }\n\n    public static void injectRaw(Registry<?> registry, Identifier tag, Function<Identifier, TagEntry> entryMaker, Identifier... values) {\n        injectRaw(registry, tag, entryMaker, Arrays.asList(values));\n    }\n\n    // -------\n\n    /**\n     * Inject the given values into the given tag, obtaining\n     * their identifiers from the given registry\n     *\n     * @param registry The registry the target tag is for\n     * @param tag      The identifier of the tag to inject into\n     * @param values   The values to inject\n     * @param <T>      The type of the target registry\n     */\n    public static <T> void inject(Registry<T> registry, Identifier tag, Collection<T> values) {\n        injectDirectReference(registry, tag, values.stream().map(registry::getKey).toList());\n    }\n\n    @SafeVarargs\n    public static <T> void inject(Registry<T> registry, Identifier tag, T... values) {\n        inject(registry, tag, Arrays.asList(values));\n    }\n\n    // -------\n\n    /**\n     * Inject the given identifiers into the given tag\n     *\n     * @param registry The registry the target tag is for\n     * @param tag      The identifier of the tag to inject into\n     * @param values   The values to inject\n     */\n    public static void injectDirectReference(Registry<?> registry, Identifier tag, Collection<Identifier> values) {\n        injectRaw(registry, tag, TagEntry::element, values);\n    }\n\n    public static void injectDirectReference(Registry<?> registry, Identifier tag, Identifier... values) {\n        injectDirectReference(registry, tag, Arrays.asList(values));\n    }\n\n    // -------\n\n    /**\n     * Inject the given tags into the given tag,\n     * effectively nesting them. This is equivalent to\n     * prefixing an entry in the tag JSON's {@code values} array\n     * with a {@code #}\n     *\n     * @param registry The registry the target tag is for\n     * @param tag      The identifier of the tag to inject into\n     * @param values   The values to inject\n     */\n    public static void injectTagReference(Registry<?> registry, Identifier tag, Collection<Identifier> values) {\n        injectRaw(registry, tag, TagEntry::tag, values);\n    }\n\n    public static void injectTagReference(Registry<?> registry, Identifier tag, Identifier... values) {\n        injectTagReference(registry, tag, Arrays.asList(values));\n    }\n\n    public record TagLocation(String type, Identifier tagId) {}\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/VectorRandomUtils.java",
    "content": "package io.wispforest.owo.util;\n\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.world.level.Level;\nimport net.minecraft.world.phys.Vec3;\n\n/**\n * Utility class for getting random offsets within a {@link Level}\n */\npublic final class VectorRandomUtils {\n\n    private VectorRandomUtils() {}\n\n    /**\n     * Generates a random point centered on the given block\n     *\n     * @param level     The level to operate in\n     * @param pos       The block position to take the center from\n     * @param deviation The size of cube from which positions are picked\n     * @return A random point no further than {@code deviation} from the center of {@code pos}\n     */\n    public static Vec3 getRandomCenteredOnBlock(Level level, BlockPos pos, double deviation) {\n        return getRandomOffset(level, new Vec3(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5), deviation);\n    }\n\n    /**\n     * Generates a random point within the given block\n     *\n     * @param level The level to operate in\n     * @param pos   The block in which to pick a point\n     * @return A random point somewhere within the bounding box of {@code pos}\n     */\n    public static Vec3 getRandomWithinBlock(Level level, BlockPos pos) {\n        return getRandomOffset(level, Vec3.atLowerCornerOf(pos).add(0.5, 0.5, 0.5), 0.5);\n    }\n\n    /**\n     * Generates a random point\n     *\n     * @param level     The level to operate in\n     * @param center    The center point\n     * @param deviation The size of cube from which positions are picked\n     * @return A random point within a cube with side length of {@code deviation} centered on {@code center}\n     */\n    public static Vec3 getRandomOffset(Level level, Vec3 center, double deviation) {\n        return getRandomOffsetSpecific(level, center, deviation, deviation, deviation);\n    }\n\n    /**\n     * Generates a random point offset from {@code center}\n     *\n     * @param level      The level to operate in\n     * @param center     The center position to start with\n     * @param deviationX The length of the selection cuboid on the x-axis\n     * @param deviationY The length of the selection cuboid on the y-axis\n     * @param deviationZ The length of the selection cuboid on the z-axis\n     * @return The generated point\n     */\n    public static Vec3 getRandomOffsetSpecific(Level level, Vec3 center, double deviationX, double deviationY, double deviationZ) {\n\n        final var r = level.getRandom();\n\n        double x = center.x() + (r.nextDouble() - 0.5) * deviationX;\n        double y = center.y() + (r.nextDouble() - 0.5) * deviationY;\n        double z = center.z() + (r.nextDouble() - 0.5) * deviationZ;\n\n        return new Vec3(x, y, z);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/VectorSerializer.java",
    "content": "package io.wispforest.owo.util;\n\nimport net.minecraft.core.Vec3i;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.DoubleTag;\nimport net.minecraft.nbt.FloatTag;\nimport net.minecraft.nbt.ListTag;\nimport net.minecraft.network.FriendlyByteBuf;\nimport net.minecraft.world.phys.Vec3;\nimport org.joml.Vector3f;\n\n/**\n * Utility class for reading and storing {@link Vec3} and\n * {@link Vector3f} from and into {@link net.minecraft.nbt.CompoundTag}\n */\npublic final class VectorSerializer {\n\n    private VectorSerializer() {}\n\n    /**\n     * Stores the given vector  as an array at the\n     * given key in the given nbt compound\n     *\n     * @param nbt   The nbt compound to serialize into\n     * @param key   The key to use\n     * @param vec3d The vector to serialize\n     * @return {@code nbt}\n     */\n    public static CompoundTag put(CompoundTag nbt, String key, Vec3 vec3d) {\n\n        ListTag vectorArray = new ListTag();\n        vectorArray.add(DoubleTag.valueOf(vec3d.x));\n        vectorArray.add(DoubleTag.valueOf(vec3d.y));\n        vectorArray.add(DoubleTag.valueOf(vec3d.z));\n\n        nbt.put(key, vectorArray);\n\n        return nbt;\n    }\n\n    /**\n     * Stores the given vector  as an array at the\n     * given key in the given nbt compound\n     *\n     * @param vec3f The vector to serialize\n     * @param nbt   The nbt compound to serialize into\n     * @param key   The key to use\n     * @return {@code nbt}\n     */\n    public static CompoundTag putf(CompoundTag nbt, String key, Vector3f vec3f) {\n\n        ListTag vectorArray = new ListTag();\n        vectorArray.add(FloatTag.valueOf(vec3f.x));\n        vectorArray.add(FloatTag.valueOf(vec3f.y));\n        vectorArray.add(FloatTag.valueOf(vec3f.z));\n\n        nbt.put(key, vectorArray);\n\n        return nbt;\n    }\n\n    /**\n     * Stores the given vector  as an array at the\n     * given key in the given nbt compound\n     *\n     * @param vec3i The vector to serialize\n     * @param nbt   The nbt compound to serialize into\n     * @param key   The key to use\n     * @return {@code nbt}\n     */\n    public static CompoundTag puti(CompoundTag nbt, String key, Vec3i vec3i) {\n\n        nbt.putIntArray(key, new int[]{vec3i.getX(), vec3i.getY(), vec3i.getZ()});\n\n        return nbt;\n    }\n\n    /**\n     * Gets the vector stored at the given key in the\n     * given nbt compound\n     *\n     * @param nbt The nbt compound to read from\n     * @param key The key the read from\n     * @return The deserialized vector\n     */\n    public static Vec3 get(CompoundTag nbt, String key) {\n\n        ListTag vectorArray = nbt.getList(key).get();\n        double x = vectorArray.getDoubleOr(0, 0d);\n        double y = vectorArray.getDoubleOr(1, 0d);\n        double z = vectorArray.getDoubleOr(2, 0d);\n\n        return new Vec3(x, y, z);\n    }\n\n    /**\n     * Gets the vector stored at the given key in the\n     * given nbt compound\n     *\n     * @param nbt The nbt compound to read from\n     * @param key The key the read from\n     * @return The deserialized vector\n     */\n    public static Vector3f getf(CompoundTag nbt, String key) {\n\n        ListTag vectorArray = nbt.getList(key).get();\n        float x = vectorArray.getFloatOr(0, 0f);\n        float y = vectorArray.getFloatOr(1, 0f);\n        float z = vectorArray.getFloatOr(2, 0f);\n\n        return new Vector3f(x, y, z);\n    }\n\n    /**\n     * Gets the vector stored at the given key in the\n     * given nbt compound\n     *\n     * @param nbt The nbt compound to read from\n     * @param key The key the read from\n     * @return The deserialized vector\n     */\n    public static Vec3i geti(CompoundTag nbt, String key) {\n\n        int[] vectorArray = nbt.getIntArray(key).get();\n        int x = vectorArray[0];\n        int y = vectorArray[1];\n        int z = vectorArray[2];\n\n        return new Vec3i(x, y, z);\n    }\n\n    /**\n     * Writes the given vector into the given packet buffer\n     *\n     * @param vec3d  The vector to write\n     * @param buffer The packet buffer to write into\n     */\n    public static void write(FriendlyByteBuf buffer, Vec3 vec3d) {\n        buffer.writeDouble(vec3d.x);\n        buffer.writeDouble(vec3d.y);\n        buffer.writeDouble(vec3d.z);\n    }\n\n    /**\n     * Writes the given vector into the given packet buffer\n     *\n     * @param vec3f  The vector to write\n     * @param buffer The packet buffer to write into\n     */\n    public static void writef(FriendlyByteBuf buffer, Vector3f vec3f) {\n        buffer.writeFloat(vec3f.x);\n        buffer.writeFloat(vec3f.y);\n        buffer.writeFloat(vec3f.z);\n    }\n\n    /**\n     * Writes the given vector into the given packet buffer\n     *\n     * @param vec3i  The vector to write\n     * @param buffer The packet buffer to write into\n     */\n    public static void writei(FriendlyByteBuf buffer, Vec3i vec3i) {\n        buffer.writeInt(vec3i.getX());\n        buffer.writeInt(vec3i.getY());\n        buffer.writeInt(vec3i.getZ());\n    }\n\n    /**\n     * Reads one vector from the given packet buffer\n     *\n     * @param buffer The buffer to read from\n     * @return The deserialized vector\n     */\n    public static Vec3 read(FriendlyByteBuf buffer) {\n        return new Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble());\n    }\n\n    /**\n     * Reads one vector from the given packet buffer\n     *\n     * @param buffer The buffer to read from\n     * @return The deserialized vector\n     */\n    public static Vector3f readf(FriendlyByteBuf buffer) {\n        return new Vector3f(buffer.readFloat(), buffer.readFloat(), buffer.readFloat());\n    }\n\n    /**\n     * Reads one vector from the given packet buffer\n     *\n     * @param buffer The buffer to read from\n     * @return The deserialized vector\n     */\n    public static Vec3i readi(FriendlyByteBuf buffer) {\n        return new Vec3i(buffer.readInt(), buffer.readInt(), buffer.readInt());\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/ViewerStack.java",
    "content": "package io.wispforest.owo.util;\n\nimport net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;\nimport net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;\nimport net.minecraft.core.component.DataComponentPatch;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\n\n// TODO: pick better name\npublic interface ViewerStack {\n    long count();\n\n    DataComponentPatch componentChanges();\n\n    record OfItem(ItemVariant item, long count) implements ViewerStack {\n        public static final OfItem EMPTY = new OfItem(ItemVariant.of(ItemStack.EMPTY), 0);\n\n        public static OfItem of(Item item) {\n            return new OfItem(ItemVariant.of(item), 1);\n        }\n\n        public static OfItem of(ItemStack stack) {\n            return new OfItem(ItemVariant.of(stack), stack.getCount());\n        }\n\n        public ItemStack asStack() {\n            return item.toStack((int) count);\n        }\n\n        @Override\n        public DataComponentPatch componentChanges() {\n            return item.getComponents();\n        }\n    }\n\n    record OfFluid(FluidVariant fluid, long count) implements ViewerStack {\n        @Override\n        public DataComponentPatch componentChanges() {\n            return fluid.getComponents();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/Wisdom.java",
    "content": "package io.wispforest.owo.util;\n\nimport com.google.common.collect.ImmutableList;\nimport io.wispforest.owo.Owo;\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.util.Util;\n\nimport java.util.List;\n\npublic final class Wisdom {\n\n    private Wisdom() {}\n\n    private static final RandomSource CRYSTAL_BALL = RandomSource.create();\n    public static final List<String> ALL_THE_WISDOM = ImmutableList.of(\n            \"assume less - mald more\",\n            \":ctft: literally infinite and counting\",\n            \"Unobtainium is usually found underground\",\n            \"Did you know that Air is made of 78 percent nitrogen and 21 percent oxygen?\",\n            \"We don't have to worry about people re-inventing Venezuela glisco, that's their problem for integrating the Bourgeoisie into their pack\",\n            \"fuck i need to update tags again\",\n            \"Don't use Forge, use Froge!\",\n            \"Noaaan: I'm talking to my inner demons. Which is all of you\",\n            \"There is a 1% chance that instead of Frog, you get Froge!\",\n            \"it seems to react to redstone\",\n            \"remember to update ubuntu, before it is too late\",\n            \"DerGeistdesMatze - i will take the l\",\n            \"idwtialsimmoedm - I didn't want to install a library so I made my own enchantment descriptions mod\",\n            \"Here at Wisp Forest© we employ Wisp Tech Support™ magic, which solves your problem when you ask\",\n            \"chyz: How could you do this to me, Blod. I loved you like a guy I don't know in real life :(\",\n            \"chyzman: No sound or Ender Dragon leakage. Although Ender Dragon sound leakage is still a thing\",\n            \"This custom packet isn't supported by Gadget - Add support via DrawPacketHandler.EVENT\",\n            \"glisco: I like this approach, cause it's stateless. And stateless approaches are always good since state management is pain - BasiqueEvangelist: I like french bread\",\n            \"dead people should put things in their grave, I have bills to pay - Blodgharm\",\n            \"Maybe we take a page out of the Wisp Forest handbook: When in doubt, steal glisco's code\",\n            \"make a man a mod, he'll be enteratined for a few minutes - teach a man to mod, and he'll mald till the end of time\",\n            \"who invited dafuqs\",\n            \"chyz: There's a snake in my prison pocket\",\n            \"Snakes are inherently funny\",\n            \"glisco makes a dollar, I make a dime, that's why I mald on company time\",\n            \"Don't trust naschhorn, the Master of Taglocks, around you or your bed\",\n            \"forge?\",\n            \"as a based person once said: it doesn't have to be balanced unless you're making a pvp focused mod\",\n            \"yes this is a tip\",\n            \"I bet DeetHunter will never see this one!\",\n            \"I think that's not the only reason Spectrum isn't compatible with Quilt\",\n            \"My Seethenhagen factory makes 331 pounds of licorice per hour\",\n            \"chyz: now i just need to painstakingly move jerry (the little tiny lil fella tiny guy man button) to correct spot when recipe book is opened\",\n            \"chyz: bro, can you stop breathing\",\n            \"Noaaan: It's surprising how much knowledge is lost by the simple fact that people don't know it\",\n            \"Blodhgarm: Did you know it took four switcheroos to implement MatrixStackTransformer in the correct package?\",\n            \"chyz: who would've thought that if you don't summon satan things go better. you can put that in wisdom if you like\",\n            \"Its called Unobtainium until you obtain it, thats the thing\",\n            \"i ate moss and i died\",\n            \"Jello was 126 years old when we added this line\",\n            \"I saw ppl complain that minor updates between big updates will ruin it for modders but I thought modders were very good and they can do what we do in a week so they should be ok with updating their mods to new versions in a day, right? :titantroll:\",\n            \"that's a CanPickUpLoot baby zombie, the most annoying thing ever. he runs around like crazy and picks up all your shit\",\n            \"blod: I think I need to take a book out of your page\",\n            \"blod: he her\",\n            \"BasiqueEvangelist: what if... iphones\",\n            \"Cheese Cheese Creeper, the newest release from MC Basic\",\n            \"THIS ROOM IS SO VANILLA IT HURTS\",\n            \"Owo.currentServer()\",\n            //Here's the 4 dots that were previously used in nested lang formatting:\n            \"....\",\n            \"Weakeys\",\n            \"glisco: It is only sometimes a hazard, so I would say it is a casino\"\n    );\n\n    public static void spread() {\n        Owo.LOGGER.info(Util.getRandom(ALL_THE_WISDOM, CRYSTAL_BALL));\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/BraidGuiRendererExtension.java",
    "content": "package io.wispforest.owo.util.pond;\n\nimport io.wispforest.owo.braid.util.BraidGuiRenderer;\n\npublic interface BraidGuiRendererExtension {\n    void owo$setTarget(BraidGuiRenderer.Target target);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/OwoAbstractContainerMenuExtension.java",
    "content": "package io.wispforest.owo.util.pond;\n\nimport io.wispforest.owo.client.screens.MenuNetworkingInternals;\nimport net.minecraft.world.entity.player.Player;\n\npublic interface OwoAbstractContainerMenuExtension {\n    void owo$attachToPlayer(Player player);\n\n    void owo$readPropertySync(MenuNetworkingInternals.SyncPropertiesPacket packet);\n\n    void owo$handlePacket(MenuNetworkingInternals.LocalPacket packet, boolean clientbound);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/OwoCreativeInventoryScreenExtensions.java",
    "content": "package io.wispforest.owo.util.pond;\n\npublic interface OwoCreativeInventoryScreenExtensions {\n    int owo$getRootX();\n\n    int owo$getRootY();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/OwoItemExtensions.java",
    "content": "package io.wispforest.owo.util.pond;\n\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.itemgroup.json.OwoItemGroupLoader;\nimport net.minecraft.world.item.CreativeModeTab;\nimport net.minecraft.world.item.Item;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.function.BiConsumer;\n\npublic interface OwoItemExtensions {\n\n    /**\n     * @return The 0-indexed tab id this item resides in, {@code -1} if none is defined\n     */\n    int owo$tab();\n\n    /**\n     * @return The function used for adding stacks of\n     * this item to an {@link OwoItemGroup} it resides in\n     */\n    BiConsumer<Item, CreativeModeTab.Output> owo$stackGenerator();\n\n    /**\n     * Sets the group of this item, used by {@link OwoItemGroupLoader} to ensure\n     * all {@code ItemGroup} references in items are correct for data-driven owo groups\n     *\n     * @param group The group to replace the current on with\n     */\n    void owo$setGroup(CreativeModeTab group);\n\n    /**\n     * @return The item group this item should reside in\n     */\n    @Nullable CreativeModeTab owo$group();\n\n    /**\n     * @return {@code true} if this item should automatically\n     * have its usage stat incremented\n     */\n    boolean owo$shouldTrackUsageStat();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/OwoScreenExtension.java",
    "content": "package io.wispforest.owo.util.pond;\n\nimport io.wispforest.owo.braid.core.AppState;\nimport io.wispforest.owo.braid.util.layers.BraidLayersBinding;\nimport io.wispforest.owo.ui.core.ParentUIComponent;\nimport io.wispforest.owo.ui.layers.Layer;\nimport net.minecraft.client.gui.screens.Screen;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.List;\n\npublic interface OwoScreenExtension {\n    List<Layer<?, ?>.Instance> owo$getInstancesView();\n    <S extends Screen, R extends ParentUIComponent> Layer<S, R>.Instance owo$getInstance(Layer<S, R> layer);\n\n    void owo$updateLayers();\n\n    // ---\n\n    void owo$setBraidLayersState(BraidLayersBinding.LayersState state);\n    @Nullable BraidLayersBinding.LayersState owo$getBraidLayersState();\n    default @Nullable AppState owo$getBraidLayersApp() {\n        var state = this.owo$getBraidLayersState();\n        if (state == null) {\n            return null;\n        }\n\n        return state.app();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/OwoSimpleRegistryExtensions.java",
    "content": "package io.wispforest.owo.util.pond;\n\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.RegistrationInfo;\nimport net.minecraft.resources.ResourceKey;\nimport org.jetbrains.annotations.ApiStatus;\n\npublic interface OwoSimpleRegistryExtensions<T> {\n\n    @ApiStatus.Internal\n    Holder.Reference<T> owo$set(int id, ResourceKey<T> arg, T object, RegistrationInfo arg2);\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/OwoSlotExtension.java",
    "content": "package io.wispforest.owo.util.pond;\n\nimport io.wispforest.owo.ui.core.PositionedRectangle;\nimport org.jetbrains.annotations.Nullable;\n\npublic interface OwoSlotExtension {\n\n    void owo$setDisabledOverride(boolean disabled);\n\n    boolean owo$getDisabledOverride();\n\n    void owo$setScissorArea(@Nullable PositionedRectangle scissor);\n\n    @Nullable PositionedRectangle owo$getScissorArea();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/OwoTextRendererExtension.java",
    "content": "package io.wispforest.owo.util.pond;\n\npublic interface OwoTextRendererExtension {\n    void owo$beginCache();\n    void owo$submitCache();\n}\n"
  },
  {
    "path": "src/main/java/io/wispforest/owo/util/pond/package-info.java",
    "content": "@ApiStatus.Internal\npackage io.wispforest.owo.util.pond;\n\nimport org.jetbrains.annotations.ApiStatus;"
  },
  {
    "path": "src/main/resources/META-INF/services/javax.annotation.processing.Processor",
    "content": "io.wispforest.owo.config.ConfigAP"
  },
  {
    "path": "src/main/resources/architectury.common.json",
    "content": "{\n  \"accessWidener\": \"owo.accesswidener\",\n  \"injected_interfaces\": {\n    \"net/minecraft/class_2487\": [\n      \"io/wispforest/endec/util/MapCarrier\"\n    ],\n    \"net/minecraft/class_11368\": [\n      \"io/wispforest/endec/util/MapCarrierDecodable\"\n    ],\n    \"net/minecraft/class_11372\": [\n      \"io/wispforest/endec/util/MapCarrierEncodable\"\n    ],\n    \"net/minecraft/class_2540\": [\n      \"io/wispforest/endec/util/EndecBuffer\"\n    ],\n    \"net/minecraft/class_339\": [\n      \"io/wispforest/owo/ui/inject/ComponentStub\"\n    ],\n    \"net/minecraft/class_342\": [\n      \"io/wispforest/owo/ui/inject/GreedyInputComponent\"\n    ],\n    \"net/minecraft/class_1703\": [\n      \"io/wispforest/owo/client/screens/OwoScreenHandler\"\n    ],\n    \"net/minecraft/class_332\": [\n      \"io/wispforest/owo/ui/util/MatrixStackTransformer\"\n    ],\n    \"net/minecraft/class_9331\\u0024class_9332\": [\n      \"io/wispforest/owo/serialization/OwoComponentTypeBuilder<TT;>\"\n    ],\n    \"net/minecraft/class_1792\\u0024class_1793\": [\n      \"io/wispforest/owo/itemgroup/OwoItemSettingsExtension\"\n    ],\n    \"net/minecraft/class_1792\": [\n      \"io/wispforest/owo/ext/OwoItem\"\n    ]\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/lang/en_us.json5",
    "content": "{\n  \"text.owo.{}\": {\n    \"itemGroup.{}\": {\n      tab_template: [\n        {index: 0},\n        {text: \" > \", color: \"gray\"},\n        {index: 1, color: \"dark_gray\"}\n      ],\n      select_hint: {text: \"Shift-click to select multiple\", color: \"gray\"}\n    },\n    \"configure_hot_reload.{}\": {\n      title: \"Configure Hot Reload\",\n      choose_file: \"Choose file\",\n      save: \"Save\",\n      model: [\n        {text: \"Model: \", color: \"yellow\"},\n        {index: 0, color: \"gray\"}\n      ],\n      \"reload_from.{}\": {\n        \"\": [\n          {text: \"Reload from: \", color: \"yellow\"},\n          {index: 0, color: \"gray\"}\n        ],\n        unset: \"Unset\"\n      }\n    },\n    \"config.{}\": {\n      \"search.{}\": {\n        \"\": \"Search...\",\n        matches: \"%d of %d\",\n        no_matches: \"No matches\"\n      },\n      must_restart: \"Some changes you made require a restart to apply\",\n      \"button.{}\": {\n        reload: \"Reload\",\n        done: \"Done\",\n        exit_minecraft: \"Exit Minecraft\",\n        ignore_restart: \"Restart later\",\n        \"range.{}\": {\n          edit_as_text: \"Edit as text\",\n          edit_with_slider: \"Edit with slider\"\n        }\n      },\n      applies_after_restart: [\n        {text: \"⏻ \", color: \"#FAEA48\"},\n        {text: \"This option applies after a restart\", color: \"gray\"}\n      ],\n      managed_by_server: [\n        {text: \"⚑ \", color: \"#EB1D36\"},\n        {text: \"This option is being managed by the server\\n   Disconnect to edit it\", color: \"gray\"}\n      ],\n      sections: {text: \"Sections\", underlined: true},\n      sections_tooltip: \"Sections\",\n      \"list.add_entry\": \"Add entry\",\n      \"boolean_toggle.{}\": {\n        enabled: [\n          \"\",\n          {text: \"[\", color: \"gray\"},\n          {text: \"✔\", color: \"#28FFBF\"},\n          {text: \"]\", color: \"gray\"},\n          \" Enabled\"\n        ],\n        disabled: [\n          \"\",\n          {text: \"[\", color: \"gray\"},\n          {text: \"❌\", color: \"#EB1D36\"},\n          {text: \"]\", color: \"gray\"},\n          \" Disabled\"\n        ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/resources/assets/owo/lang/tt_ru.json5",
    "content": "{\n  \"modmenu.{}.owo\": {\n    descriptionTranslation: \"әйе бу начар мин беләм\",\n    summaryTranslation: \"әйе бу начар мин беләм\"\n  },\n  \"test.owo.{}\": {\n    \"itemGroup.{}\": {\n      tab_template: [\n        {index: 0},\n        {text: \" > \", color: \"gray\"},\n        {index: 1, color: \"dark_gray\"}\n      ],\n      select_hint: {text: \"Берничә сайлау өчен Shift төймәсенә басыгыз\", color: \"gray\"}\n    },\n    \"configure_hot_reload.{}\": {\n      title: \"Кайнар яңадан йөкләү\",\n      choose_file: \"Файлны сайлау\",\n      save: \"Саклау\",\n      model: [\n        {text: \"Модель: \", color: \"yellow\"},\n        {index: 0, color: \"gray\"}\n      ],\n      \"reload_from.unset\": \"Сайланмаган\",\n      reload_from: [\n        {text: \"Яңадан йөкләү урыны: \", color: \"yellow\"},\n        {index: 0, color: \"gray\"}\n      ]\n    },\n    \"config.{}\": {\n      search: \"Эзләү.{}\",\n      \"search.{}\": {\n        matches: \"%$2d нәтиҗәдән %$1d нәтиҗә\",\n        no_matches: \"Нәтиҗәләр юк\"\n      },\n      must_restart: \"Сез керткән кайбер үзгәрешләр куллану өчен яңадан йөкләүне таләп итә\",\n      \"button.{}\": {\n        exit_minecraft: \"Minecraft-тан чыгу\",\n        ignore_restart: \"Яңадан кушу соңрак\",\n        \"range.edit_{}\": {\n          as_text: \"Текст кебек үзгәртү\",\n          with_slider: \"Шудырма белән үзгәртү\"\n        },\n        reload: \"↻\",\n        done: \"Булды\"\n      },\n      applies_after_restart: [\n        {text: \"⏻ \", color: \"#FAEA48\"},\n        {text: \"Бу көйләү яңадан йөкләүдән соң кулланыла\", color: \"gray\"}\n      ],\n      managed_by_server: [\n        {text: \"⚑ \", color: \"#EB1D36\"},\n        {text: \"Бу көйләү сервер белән идарә ителә\\n   Аны үзгәртү өчен өзелегез\", color: \"gray\"}\n      ],\n      sections_tooltip: \"Бүлекләр\",\n      sections: {text: \"Бүлекләр\", underlined: true},\n      \"list.add_entry\": \"Элементны өстәү\",\n      \"boolean_toggle.{}\": {\n        enabled: [\n          \"\",\n          {text: \"[\", color: \"gray\"},\n          {text: \"✔\", color: \"#28FFBF\"},\n          {text: \"]\", color: \"gray\"},\n          \" Кушык\"\n        ],\n        disabled: [\n          \"\",\n          {text: \"[\", color: \"gray\"},\n          {text: \"❌\", color: \"#EB1D36\"},\n          {text: \"]\", color: \"gray\"},\n          \" Сүнек\"\n        ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/braid_combobox/active.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/braid_combobox.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 192,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 18,\n    \"height\": 4\n  },\n  \"center_patch_size\": {\n    \"width\": 28,\n    \"height\": 56\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/braid_combobox/disabled.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/braid_combobox.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 192,\n  \"v\": 128,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 3,\n    \"height\": 3\n  },\n  \"center_patch_size\": {\n    \"width\": 58,\n    \"height\": 58\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/braid_combobox/hovered.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/braid_combobox.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 192,\n  \"v\": 64,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 18,\n    \"height\": 4\n  },\n  \"center_patch_size\": {\n    \"width\": 28,\n    \"height\": 56\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/braid_debug_focused.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/braid_debug_focused.png\",\n  \"texture_width\": 128,\n  \"texture_height\": 128,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 4,\n    \"height\": 4\n  },\n  \"center_patch_size\": {\n    \"width\": 120,\n    \"height\": 120\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/braid_debug_highlighted.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/braid_debug_highlighted.png\",\n  \"texture_width\": 128,\n  \"texture_height\": 128,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 4,\n    \"height\": 4\n  },\n  \"center_patch_size\": {\n    \"width\": 120,\n    \"height\": 120\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/braid_inspector_selected.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/braid_inspector_selected.png\",\n  \"texture_width\": 5,\n  \"texture_height\": 5,\n  \"repeat\": false,\n  \"corner_patch_size\": {\n    \"width\": 2,\n    \"height\": 2\n  },\n  \"center_patch_size\": {\n    \"width\": 1,\n    \"height\": 1\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/button/active.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/buttons.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 192,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 3,\n    \"height\": 3\n  },\n  \"center_patch_size\": {\n    \"width\": 58,\n    \"height\": 58\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/button/disabled.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/buttons.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 192,\n  \"v\": 128,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 3,\n    \"height\": 3\n  },\n  \"center_patch_size\": {\n    \"width\": 58,\n    \"height\": 58\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/button/hovered.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/buttons.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 192,\n  \"v\": 64,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 3,\n    \"height\": 3\n  },\n  \"center_patch_size\": {\n    \"width\": 58,\n    \"height\": 58\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/panel/dark.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/dark_panel.png\",\n  \"texture_width\": 16,\n  \"texture_height\": 16,\n  \"repeat\": false,\n  \"patch_size\": {\n    \"width\": 5,\n    \"height\": 5\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/panel/default.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/panel.png\",\n  \"texture_width\": 16,\n  \"texture_height\": 16,\n  \"repeat\": false,\n  \"patch_size\": {\n    \"width\": 5,\n    \"height\": 5\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/panel/inset.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/panel_inset.png\",\n  \"texture_width\": 16,\n  \"texture_height\": 16,\n  \"repeat\": false,\n  \"patch_size\": {\n    \"width\": 5,\n    \"height\": 5\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/scrollbar/track.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/scrollbar.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 32,\n  \"u\": 26,\n  \"repeat\": false,\n  \"corner_patch_size\": {\n    \"width\": 1,\n    \"height\": 1\n  },\n  \"center_patch_size\": {\n    \"width\": 4,\n    \"height\": 30\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/scrollbar/vanilla_flat.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/scrollbar.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 32,\n  \"u\": 33,\n  \"repeat\": false,\n  \"corner_patch_size\": {\n    \"width\": 1,\n    \"height\": 1\n  },\n  \"center_patch_size\": {\n    \"width\": 14,\n    \"height\": 30\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/scrollbar/vanilla_horizontal.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/scrollbar.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 32,\n  \"v\": 16,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 2,\n    \"height\": 2\n  },\n  \"center_patch_size\": {\n    \"width\": 8,\n    \"height\": 12\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/scrollbar/vanilla_horizontal_disabled.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/scrollbar.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 32,\n  \"u\": 13,\n  \"v\": 16,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 2,\n    \"height\": 2\n  },\n  \"center_patch_size\": {\n    \"width\": 8,\n    \"height\": 12\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/scrollbar/vanilla_vertical.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/scrollbar.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 32,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 2,\n    \"height\": 2\n  },\n  \"center_patch_size\": {\n    \"width\": 8,\n    \"height\": 12\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/scrollbar/vanilla_vertical_disabled.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/scrollbar.png\",\n  \"texture_width\": 64,\n  \"texture_height\": 32,\n  \"u\": 13,\n  \"repeat\": true,\n  \"corner_patch_size\": {\n    \"width\": 2,\n    \"height\": 2\n  },\n  \"center_patch_size\": {\n    \"width\": 8,\n    \"height\": 12\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/nine_patch_textures/slim_slider_track.json",
    "content": "{\n  \"texture\": \"owo:textures/gui/slim_slider.png\",\n  \"texture_width\": 16,\n  \"texture_height\": 16,\n  \"repeat\": false,\n  \"corner_patch_size\": {\n    \"width\": 1,\n    \"height\": 1\n  },\n  \"center_patch_size\": {\n    \"width\": 14,\n    \"height\": 1\n  }\n}"
  },
  {
    "path": "src/main/resources/assets/owo/owo_ui/config.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"vertical\">\n            <children>\n                <flow-layout direction=\"vertical\">\n                    <children>\n                        <label id=\"title\">\n                            <text translate=\"true\"/>\n                            <shadow>true</shadow>\n\n                            <margins>\n                                <vertical>10</vertical>\n                            </margins>\n                        </label>\n                    </children>\n\n                    <vertical-alignment>center</vertical-alignment>\n                </flow-layout>\n\n                <stack-layout>\n                    <children>\n                        <flow-layout direction=\"horizontal\" id=\"main-panel\">\n                            <children>\n                                <flow-layout direction=\"vertical\" id=\"option-panel-container\">\n                                    <children>\n                                        <scroll direction=\"vertical\" id=\"option-panel-scroll\">\n                                            <flow-layout direction=\"vertical\" id=\"option-panel\">\n                                                <children/>\n                                                <padding>\n                                                    <right>3</right>\n                                                </padding>\n                                            </flow-layout>\n\n                                            <scrollbar-thiccness>3</scrollbar-thiccness>\n\n                                            <sizing>\n                                                <horizontal method=\"fill\">100</horizontal>\n                                                <vertical method=\"fill\">100</vertical>\n                                            </sizing>\n\n                                            <padding>\n                                                <all>1</all>\n                                            </padding>\n                                        </scroll>\n                                    </children>\n\n                                    <sizing>\n                                        <horizontal method=\"expand\">100</horizontal>\n                                        <vertical method=\"fill\">100</vertical>\n                                    </sizing>\n                                </flow-layout>\n                            </children>\n\n                            <padding>\n                                <horizontal>50</horizontal>\n                            </padding>\n\n                            <sizing>\n                                <horizontal method=\"fill\">100</horizontal>\n                                <vertical method=\"fill\">100</vertical>\n                            </sizing>\n\n                            <surface>\n                                <flat>#77000000</flat>\n                                <outline>#99121212</outline>\n                            </surface>\n                        </flow-layout>\n                    </children>\n\n                    <padding>\n                        <all>1</all>\n                    </padding>\n\n                    <sizing>\n                        <horizontal method=\"fill\">101</horizontal>\n                        <vertical method=\"expand\">100</vertical>\n                    </sizing>\n\n                    <surface>\n                        <outline>#33FFFFFF</outline>\n                    </surface>\n                </stack-layout>\n\n                <flow-layout direction=\"horizontal\">\n                    <children>\n                        <flow-layout direction=\"horizontal\">\n                            <children>\n                                <texture texture=\"owo:textures/gui/config_search.png\"\n                                         texture-width=\"16\" texture-height=\"16\"\n                                         region-width=\"16\" region-height=\"16\">\n                                    <margins>\n                                        <all>2</all>\n                                    </margins>\n                                </texture>\n\n                                <flow-layout direction=\"horizontal\">\n                                    <children>\n                                        <text-box id=\"search-field\">\n                                            <show-background>false</show-background>\n                                            <max-length>128</max-length>\n\n                                            <sizing>\n                                                <horizontal method=\"fill\">50</horizontal>\n                                                <vertical method=\"fixed\">9</vertical>\n                                            </sizing>\n                                        </text-box>\n\n                                        <label id=\"search-match-indicator\">\n                                            <margins>\n                                                <horizontal>5</horizontal>\n                                            </margins>\n                                        </label>\n                                    </children>\n\n                                    <surface>\n                                        <vanilla-translucent/>\n                                    </surface>\n\n                                    <vertical-alignment>center</vertical-alignment>\n\n                                    <padding>\n                                        <all>3</all>\n                                    </padding>\n                                </flow-layout>\n                            </children>\n\n                            <vertical-alignment>center</vertical-alignment>\n\n                            <positioning type=\"relative\">0,50</positioning>\n                        </flow-layout>\n\n                        <button id=\"reload-button\">\n                            <text translate=\"true\">text.owo.config.button.reload</text>\n                            <sizing>\n                                <horizontal method=\"fill\">10</horizontal>\n                            </sizing>\n                            <margins>\n                                <right>5</right>\n                            </margins>\n                        </button>\n\n                        <button id=\"done-button\">\n                            <text translate=\"true\">text.owo.config.button.done</text>\n                            <sizing>\n                                <horizontal method=\"fill\">10</horizontal>\n                            </sizing>\n                        </button>\n                    </children>\n\n                    <horizontal-alignment>right</horizontal-alignment>\n                    <vertical-alignment>center</vertical-alignment>\n\n                    <margins>\n                        <vertical>5</vertical>\n                    </margins>\n\n                    <padding>\n                        <horizontal>50</horizontal>\n                    </padding>\n\n                    <sizing>\n                        <horizontal method=\"fill\">100</horizontal>\n                    </sizing>\n                </flow-layout>\n            </children>\n\n            <vertical-alignment>center</vertical-alignment>\n            <horizontal-alignment>center</horizontal-alignment>\n\n            <surface>\n                <options-background/>\n            </surface>\n\n            <sizing>\n                <horizontal method=\"fill\">100</horizontal>\n                <vertical method=\"fill\">100</vertical>\n            </sizing>\n        </flow-layout>\n    </components>\n\n    <templates>\n        <template name=\"section-header\">\n            <flow-layout direction=\"horizontal\">\n                <children>\n                    <box>\n                        <sizing>\n                            <vertical method=\"fixed\">2</vertical>\n                            <horizontal method=\"fill\">35</horizontal>\n                        </sizing>\n\n                        <start-color>#FFFFFFFF</start-color>\n                        <end-color>#00000000</end-color>\n\n                        <direction>right-to-left</direction>\n\n                        <fill>true</fill>\n                    </box>\n                    <label id=\"header\">\n                        <margins>\n                            <horizontal>5</horizontal>\n                        </margins>\n                    </label>\n                    <box>\n                        <sizing>\n                            <vertical method=\"fixed\">2</vertical>\n                            <horizontal method=\"fill\">35</horizontal>\n                        </sizing>\n\n                        <start-color>#FFFFFFFF</start-color>\n                        <end-color>#00000000</end-color>\n\n                        <direction>left-to-right</direction>\n\n                        <fill>true</fill>\n                    </box>\n                </children>\n\n                <horizontal-alignment>center</horizontal-alignment>\n                <vertical-alignment>center</vertical-alignment>\n\n                <margins>\n                    <top>10</top>\n                </margins>\n\n                <sizing>\n                    <horizontal method=\"fill\">100</horizontal>\n                    <vertical method=\"fixed\">20</vertical>\n                </sizing>\n            </flow-layout>\n        </template>\n\n        <template name=\"section-buttons\">\n            <flow-layout direction=\"vertical\">\n                <children>\n                    <label>\n                        <text translate=\"true\">text.owo.config.sections</text>\n\n                        <positioning type=\"relative\">50,0</positioning>\n\n                        <margins>\n                            <top>15</top>\n                        </margins>\n                    </label>\n                </children>\n\n                <vertical-alignment>center</vertical-alignment>\n                <horizontal-alignment>center</horizontal-alignment>\n\n                <padding>\n                    <horizontal>2</horizontal>\n                </padding>\n\n                <sizing>\n                    <horizontal method=\"fixed\">0</horizontal>\n                    <vertical method=\"fill\">100</vertical>\n                </sizing>\n            </flow-layout>\n        </template>\n\n        <template name=\"config-option-base\">\n            <flow-layout direction=\"horizontal\">\n                <children>\n                    <label id=\"option-name\">\n                        <text translate=\"true\">{{config-option-name}}</text>\n                        <positioning type=\"relative\">0,50</positioning>\n                        <shadow>true</shadow>\n                    </label>\n\n                    <template-child id=\"controls\">\n                        <positioning type=\"relative\">100,50</positioning>\n                        <vertical-alignment>center</vertical-alignment>\n                    </template-child>\n                </children>\n\n                <sizing>\n                    <horizontal method=\"fill\">100</horizontal>\n                    <vertical method=\"fixed\">32</vertical>\n                </sizing>\n\n                <padding>\n                    <all>5</all>\n                </padding>\n            </flow-layout>\n        </template>\n\n        <template name=\"config-option\">\n            <template name=\"config-option-base\">\n                <child id=\"controls\">\n                    <flow-layout direction=\"horizontal\" id=\"controls-flow\">\n                        <children>\n                            <template-child id=\"value-container\">\n                                <sizing>\n                                    <horizontal method=\"fixed\">120</horizontal>\n                                </sizing>\n                            </template-child>\n\n                            <button id=\"reset-button\">\n                                <text>⇄</text>\n                                <margins>\n                                    <horizontal>5</horizontal>\n                                </margins>\n                            </button>\n                        </children>\n                    </flow-layout>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"text-box-config-option\">\n            <template name=\"config-option\">\n                <child id=\"value-container\">\n                    <config-text-box id=\"value-box\">\n                        <text>{{config-option-value}}</text>\n                    </config-text-box>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"boolean-toggle-config-option\">\n            <template name=\"config-option\">\n                <child id=\"value-container\">\n                    <config-toggle-button id=\"toggle-button\"/>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"enum-config-option\">\n            <template name=\"config-option\">\n                <child id=\"value-container\">\n                    <config-enum-button id=\"enum-button\"/>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"range-config-option\">\n            <template name=\"config-option-base\">\n                <child id=\"controls\">\n                    <flow-layout direction=\"horizontal\" id=\"controls-flow\">\n                        <children>\n                            <button id=\"toggle-button\">\n                                <text>✎</text>\n                                <tooltip-text translate=\"true\">text.owo.config.button.range.edit_as_text</tooltip-text>\n\n                                <renderer>\n                                    <flat color=\"#00000000\" hovered-color=\"#77000000\" disabled-color=\"#00000000\"/>\n                                </renderer>\n\n                                <sizing>\n                                    <vertical method=\"fixed\">16</vertical>\n                                </sizing>\n                            </button>\n\n                            <flow-layout direction=\"horizontal\" id=\"slider-controls\">\n                                <children>\n                                    <config-slider id=\"value-slider\">\n                                        <margins>\n                                            <all>1</all>\n                                        </margins>\n                                        <sizing>\n                                            <horizontal method=\"fixed\">120</horizontal>\n                                        </sizing>\n                                    </config-slider>\n\n                                    <button id=\"reset-button\">\n                                        <text>⇄</text>\n                                        <margins>\n                                            <horizontal>5</horizontal>\n                                        </margins>\n                                    </button>\n                                </children>\n\n                                <vertical-alignment>center</vertical-alignment>\n                            </flow-layout>\n                        </children>\n\n                        <gap>2</gap>\n                    </flow-layout>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"color-picker-panel\">\n            <flow-layout direction=\"vertical\">\n                <children>\n                    <label>\n                        <text>Choose color</text>\n                        <shadow>true</shadow>\n                        <margins>\n                            <top>3</top>\n                        </margins>\n                    </label>\n\n                    <color-picker id=\"color-picker\">\n                        <show-alpha>{{with-alpha}}</show-alpha>\n                        <selected-color>{{color}}</selected-color>\n                        <sizing>\n                            <horizontal method=\"fixed\">160</horizontal>\n                            <vertical method=\"fixed\">100</vertical>\n                        </sizing>\n                    </color-picker>\n\n                    <flow-layout direction=\"horizontal\">\n                        <children>\n                            <box>\n                                <fill>true</fill>\n                                <color>{{color}}</color>\n                                <sizing>\n                                    <horizontal method=\"fixed\">80</horizontal>\n                                    <vertical method=\"fixed\">15</vertical>\n                                </sizing>\n                            </box>\n                            <box id=\"current-color\">\n                                <fill>true</fill>\n                                <color>{{color}}</color>\n                                <sizing>\n                                    <horizontal method=\"fixed\">80</horizontal>\n                                    <vertical method=\"fixed\">15</vertical>\n                                </sizing>\n                            </box>\n                        </children>\n                    </flow-layout>\n\n                    <flow-layout direction=\"horizontal\">\n                        <children>\n                            <button id=\"cancel-button\">\n                                <text>❌</text>\n                                <sizing>\n                                    <horizontal method=\"fixed\">50</horizontal>\n                                    <vertical method=\"fixed\">14</vertical>\n                                </sizing>\n                            </button>\n\n                            <button id=\"confirm-button\">\n                                <text>✔</text>\n                                <sizing>\n                                    <horizontal method=\"fixed\">50</horizontal>\n                                    <vertical method=\"fixed\">14</vertical>\n                                </sizing>\n                            </button>\n                        </children>\n\n                        <gap>10</gap>\n                    </flow-layout>\n                </children>\n\n                <horizontal-alignment>center</horizontal-alignment>\n\n                <gap>5</gap>\n                <padding>\n                    <all>5</all>\n                </padding>\n                <surface>\n                    <panel dark=\"true\"/>\n                </surface>\n            </flow-layout>\n        </template>\n\n    </templates>\n</owo-ui>\n\n"
  },
  {
    "path": "src/main/resources/assets/owo/owo_ui/configure_hot_reload.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"https://raw.githubusercontent.com/wisp-forest/owo-lib/1.19.4/owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"vertical\">\n            <children>\n                <flow-layout direction=\"vertical\">\n                    <children>\n                        <label>\n                            <text translate=\"true\">text.owo.configure_hot_reload.title</text>\n                            <shadow>true</shadow>\n                        </label>\n\n                        <flow-layout direction=\"vertical\">\n                            <children>\n                                <label id=\"ui-model-label\"/>\n                                <label id=\"file-name-label\">\n                                    <max-width>250</max-width>\n                                </label>\n                            </children>\n                        </flow-layout>\n\n                        <flow-layout direction=\"horizontal\">\n                            <children>\n                                <template name=\"button\">\n                                    <id>choose-button</id>\n                                    <message>text.owo.configure_hot_reload.choose_file</message>\n                                </template>\n\n                                <template name=\"button\">\n                                    <id>save-button</id>\n                                    <message>text.owo.configure_hot_reload.save</message>\n                                </template>\n                            </children>\n\n                            <gap>10</gap>\n                        </flow-layout>\n\n                        <label id=\"close-label\">\n                            <text>❌</text>\n                            <positioning type=\"relative\">100,0</positioning>\n                            <cursor-style>hand</cursor-style>\n                        </label>\n                    </children>\n\n                    <horizontal-alignment>center</horizontal-alignment>\n\n                    <padding>\n                        <all>5</all>\n                    </padding>\n\n                    <gap>5</gap>\n\n                    <surface>\n                        <flat>#77000000</flat>\n                        <outline>#FF121212</outline>\n                    </surface>\n                </flow-layout>\n            </children>\n\n            <surface>\n                <vanilla-translucent/>\n            </surface>\n\n            <vertical-alignment>center</vertical-alignment>\n            <horizontal-alignment>center</horizontal-alignment>\n        </flow-layout>\n    </components>\n\n    <templates>\n        <template name=\"button\">\n            <button id=\"{{id}}\">\n                <text translate=\"true\">{{message}}</text>\n                <sizing>\n                    <horizontal method=\"fixed\">70</horizontal>\n                    <vertical method=\"fixed\">15</vertical>\n                </sizing>\n\n                <renderer>\n                    <flat color=\"#77000000\" hovered-color=\"#AA000000\" disabled-color=\"black\"/>\n                </renderer>\n            </button>\n        </template>\n    </templates>\n</owo-ui>"
  },
  {
    "path": "src/main/resources/assets/owo/owo_ui/restart_required.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"https://raw.githubusercontent.com/wisp-forest/owo-lib/1.19.4/owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"vertical\">\n            <children>\n                <label>\n                    <text translate=\"true\">text.owo.config.must_restart</text>\n                    <shadow>true</shadow>\n                </label>\n\n                <flow-layout direction=\"horizontal\">\n                    <children>\n                        <template name=\"action-button\">\n                            <id>exit-button</id>\n                            <action>exit_minecraft</action>\n                        </template>\n\n                        <template name=\"action-button\">\n                            <id>ignore-button</id>\n                            <action>ignore_restart</action>\n                        </template>\n                    </children>\n\n                    <margins>\n                        <top>25</top>\n                    </margins>\n\n                </flow-layout>\n            </children>\n\n            <surface>\n                <vanilla-translucent/>\n            </surface>\n\n            <vertical-alignment>center</vertical-alignment>\n            <horizontal-alignment>center</horizontal-alignment>\n        </flow-layout>\n    </components>\n\n    <templates>\n        <action-button>\n            <button id=\"{{id}}\">\n                <text translate=\"true\">text.owo.config.button.{{action}}</text>\n\n                <sizing>\n                    <horizontal method=\"content\">5</horizontal>\n                </sizing>\n\n                <margins>\n                    <all>5</all>\n                </margins>\n            </button>\n        </action-button>\n    </templates>\n</owo-ui>"
  },
  {
    "path": "src/main/resources/assets/owo/shaders/core/blur.fsh",
    "content": "#version 150\n\n#moj_import <minecraft:dynamictransforms.glsl>\n\nuniform sampler2D InputSampler;\nlayout(std140) uniform BlurSettings {\n    vec2 InputResolution;\n    float Directions;\n    float Quality;\n    float Size;\n};\n\nout vec4 fragColor;\n\n// shader adapted from https://www.shadertoy.com/view/Xltfzj\n\nvoid main() {\n    #define TAU 6.28318530718\n\n    vec2 Radius = Size / InputResolution.xy;\n\n    // Normalized pixel coordinates (from 0 to 1)\n    vec2 uv = gl_FragCoord.xy / InputResolution.xy;\n    // Pixel colour\n    vec4 Color = texture(InputSampler, uv);\n\n    // Blur calculations\n    for (float d = 0.0; d < TAU; d += TAU / Directions) {\n        for (float i = 1.0 / Quality; i <= 1.0; i += 1.0 / Quality) {\n            Color += texture(InputSampler, uv + vec2(cos(d), sin(d)) * Radius * i);\n        }\n    }\n\n    // Output to screen\n    Color /= Quality * Directions;\n    fragColor = Color * ColorModulator;\n}\n"
  },
  {
    "path": "src/main/resources/assets/owo/shaders/core/blur.vsh",
    "content": "#version 150\n\n#moj_import <minecraft:dynamictransforms.glsl>\n#moj_import <minecraft:projection.glsl>\n\nin vec3 Position;\n\nvoid main() {\n    gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);\n}\n"
  },
  {
    "path": "src/main/resources/assets/owo/shaders/core/spectrum.fsh",
    "content": "#version 150\n\n// Can't moj_import in things used during startup, when resource packs don't exist.\n// This is a copy of dynamicimports.glsl\nlayout(std140) uniform DynamicTransforms {\n    mat4 ModelViewMat;\n    vec4 ColorModulator;\n    vec3 ModelOffset;\n    mat4 TextureMat;\n};\n\nin vec4 vertexColor;\nout vec4 fragColor;\n\nvec3 hsv2rgb(vec3 hsv) {\n    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n    vec3 p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www);\n    return hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y);\n}\n\nvoid main() {\n    fragColor = vec4(\n        hsv2rgb(vertexColor.xyz).xyz,\n        vertexColor.w\n    ) * ColorModulator;\n}\n"
  },
  {
    "path": "src/main/resources/assets/owo/sounds.json",
    "content": "{\n  \"ui.owo.interaction\": {\n    \"sounds\": [\n      \"owo:ui_interaction\"\n    ]\n  }\n}"
  },
  {
    "path": "src/main/resources/fabric.mod.json",
    "content": "{\n  \"schemaVersion\": 1,\n  \"id\": \"owo\",\n  \"version\": \"${version}\",\n  \"name\": \"oωo\",\n  \"description\": \"yes its bad i know thanks\",\n  \"authors\": [\n    \"glisco\"\n  ],\n  \"contributors\": [\n    \"Blodhgarm\",\n    \"BasiqueEvangelist\",\n    \"Noaaan\",\n    \"chyzman\"\n  ],\n  \"contact\": {},\n  \"license\": \"MIT\",\n  \"icon\": \"assets/owo/icon.png\",\n  \"environment\": \"*\",\n  \"entrypoints\": {\n    \"client\": [\n      \"io.wispforest.owo.client.OwoClient\"\n    ],\n    \"main\": [\n      \"io.wispforest.owo.Owo\"\n    ],\n    \"rei_client\": [\n      \"io.wispforest.owo.compat.rei.OwoReiPlugin\"\n    ],\n    \"emi\": [\n      \"io.wispforest.owo.compat.emi.OwoEmiPlugin\"\n    ],\n    \"modmenu\": [\n      \"io.wispforest.owo.compat.modmenu.OwoModMenuPlugin\"\n    ]\n  },\n  \"mixins\": [\n    \"owo.mixins.json\"\n  ],\n  \"provides\": [\n    \"owo-impl\",\n    \"owo-lib\"\n  ],\n  \"accessWidener\": \"owo.accesswidener\",\n  \"depends\": {\n    \"fabricloader\": \">=0.15.0\",\n    \"fabric-api\": \">=0.100\",\n    \"minecraft\": \">=1.21.6\"\n  },\n  \"custom\": {\n    \"modmenu\": {\n      \"links\": {\n        \"modmenu.discord\": \"https://discord.gg/xrwHKktV2d\"\n      },\n      \"badges\": [\n        \"library\"\n      ]\n    },\n    \"loom:injected_interfaces\": {\n      \"net/minecraft/class_2487\": [\n        \"io/wispforest/endec/util/MapCarrier\"\n      ],\n      \"net/minecraft/class_11368\": [\n        \"io/wispforest/endec/util/MapCarrierDecodable\"\n      ],\n      \"net/minecraft/class_11372\": [\n        \"io/wispforest/endec/util/MapCarrierEncodable\"\n      ],\n      \"net/minecraft/class_2540\": [\n        \"io/wispforest/endec/util/EndecBuffer\"\n      ],\n      \"net/minecraft/class_339\": [\n        \"io/wispforest/owo/ui/inject/UIComponentStub\"\n      ],\n      \"net/minecraft/class_342\": [\n        \"io/wispforest/owo/ui/inject/GreedyInputUIComponent\"\n      ],\n      \"net/minecraft/class_1703\": [\n        \"io/wispforest/owo/client/screens/OwoAbstractContainerMenu\"\n      ],\n      \"net/minecraft/class_332\": [\n        \"io/wispforest/owo/ui/util/MatrixStackTransformer\"\n      ],\n      \"net/minecraft/class_9331\\u0024class_9332\": [\n        \"io/wispforest/owo/serialization/OwoDataComponentTypeBuilder<TT;>\"\n      ],\n      \"net/minecraft/class_1792\\u0024class_1793\": [\n        \"io/wispforest/owo/itemgroup/OwoItemSettingsExtension\"\n      ],\n      \"net/minecraft/class_1792\": [\n        \"io/wispforest/owo/ext/OwoItem\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/resources/owo-json5",
    "content": ""
  },
  {
    "path": "src/main/resources/owo.accesswidener",
    "content": "accessWidener v2 named\n\naccessible method net/minecraft/client/gui/screens/Screen addRenderableWidget (Lnet/minecraft/client/gui/components/events/GuiEventListener;)Lnet/minecraft/client/gui/components/events/GuiEventListener;\n\nextendable method net/minecraft/world/item/CreativeModeTab <init> (Lnet/minecraft/world/item/CreativeModeTab$Row;ILnet/minecraft/world/item/CreativeModeTab$Type;Lnet/minecraft/network/chat/Component;Ljava/util/function/Supplier;Lnet/minecraft/world/item/CreativeModeTab$DisplayItemsGenerator;)V\ntransitive-accessible class net/minecraft/world/item/CreativeModeTab$Output\ntransitive-accessible class net/minecraft/world/item/CreativeModeTab$ItemDisplayBuilder\n\ntransitive-accessible class net/minecraft/client/gui/GuiGraphics$ScissorStack\nextendable method net/minecraft/client/gui/GuiGraphics setTooltipForNextFrameInternal (Lnet/minecraft/client/gui/Font;Ljava/util/List;IILnet/minecraft/client/gui/screens/inventory/tooltip/ClientTooltipPositioner;Lnet/minecraft/resources/Identifier;Z)V\ntransitive-extendable method net/minecraft/client/gui/components/Checkbox <init> (IIILnet/minecraft/network/chat/Component;Lnet/minecraft/client/gui/Font;ZLnet/minecraft/client/gui/components/Checkbox$OnValueChange;)V\n\naccessible class net/minecraft/client/gui/render/GuiRenderer$Draw\n\naccessible class net/minecraft/client/gui/components/toasts/ToastManager$ToastInstance\n\naccessible class net/minecraft/resources/RegistryOps$HolderLookupAdapter\naccessible method net/minecraft/client/gui/components/MultiLineEditBox <init> (Lnet/minecraft/client/gui/Font;IIIILnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/Component;IZIZZ)V\n\nextendable method net/minecraft/client/gui/screens/inventory/AbstractContainerScreen renderSlotHighlightBack (Lnet/minecraft/client/gui/GuiGraphics;)V\nextendable method net/minecraft/client/gui/screens/inventory/AbstractContainerScreen renderSlotHighlightFront (Lnet/minecraft/client/gui/GuiGraphics;)V\nextendable method net/minecraft/client/gui/screens/inventory/AbstractContainerScreen renderFloatingItem (Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/item/ItemStack;IILjava/lang/String;)V\n\naccessible class net/minecraft/core/component/DataComponentMap$Builder$SimpleMap\n\naccessible class net/minecraft/server/packs/resources/FallbackResourceManager$PackEntry\naccessible field net/minecraft/server/packs/resources/FallbackResourceManager$PackEntry resources Lnet/minecraft/server/packs/PackResources;\n"
  },
  {
    "path": "src/main/resources/owo.mixins.json",
    "content": "{\n  \"required\": true,\n  \"minVersion\": \"0.8\",\n  \"package\": \"io.wispforest.owo.mixin\",\n  \"compatibilityLevel\": \"JAVA_16\",\n  \"mixins\": [\n    \"ConnectionMixin\",\n    \"Copenhagen\",\n    \"AbstractContainerMenuInvoker\",\n    \"AbstractContainerMenuMixin\",\n    \"ServerCommonPacketListenerImplAccessor\",\n    \"ServerPlayerMixin\",\n    \"ServerPlayerGameModeMixin\",\n    \"SetComponentsFunctionAccessor\",\n    \"TagLoaderMixin\",\n    \"braid.ClickableStyleFinderAccessor\",\n    \"braid.RenderTypeInvoker\",\n    \"ext.ItemMixin\",\n    \"ext.ItemStackMixin\",\n    \"ext.PatchedDataComponentMapAccessor\",\n    \"ext.PatchedDataComponentMapMixin\",\n    \"extension.SimpleJsonResourceReloadListenerMixin\",\n    \"extension.json5.LanguageReaderMixin\",\n    \"extension.json5.MultiPackResourceManagerMixin\",\n    \"extension.json5.FallbackResourceManagerMixin\",\n    \"extension.json5.FileToIdConverterMixin\",\n    \"extension.recipe.ResultSlotMixin\",\n    \"extension.recipe.RecipeManagerAccessor\",\n    \"itemgroup.CreativeModeTabAccessor\",\n    \"itemgroup.ItemMixin\",\n    \"itemgroup.ItemSettingsMixin\",\n    \"registry.ReferenceAccessor\",\n    \"registry.MappedRegistryMixin\",\n    \"serialization.CachedRegistryInfoGetterAccessor\",\n    \"serialization.DataComponentTypeBuilderMixin\",\n    \"serialization.DataResultMixin\",\n    \"serialization.DataResultMixin$DataResultErrorMixin\",\n    \"serialization.DelegatingOpsAccessor\",\n    \"serialization.CompoundTagMixin\",\n    \"serialization.FriendlyByteBufMixin\",\n    \"serialization.ValueInputMixin\",\n    \"serialization.RegistryOpsAccessor\",\n    \"serialization.ValueOutputMixin\",\n    \"serialization.TagValueInputMixin\",\n    \"serialization.TagValueOutputMixin\",\n    \"text.LanguageMixin\",\n    \"text.ComponentSerializationMixin\",\n    \"text.TranslatableContentsAccessor\",\n    \"text.TranslatableContentsMixin\",\n    \"text.stapi.SystemDelegatedLanguageFixin\",\n    \"tweaks.EulaMixin\",\n    \"tweaks.LevelSettingsMixin\",\n    \"ui.SlotAccessor\",\n    \"ui.SlotMixin\",\n    \"ui.access.BlockEntityAccessor\"\n  ],\n  \"client\": [\n    \"ClientCommonPacketListenerImplAccessor\",\n    \"ClientConfigurationPacketListenerImplMixin\",\n    \"ClientHandshakePacketListenerImplAccessor\",\n    \"GuiGraphicsMixin\",\n    \"MinecraftMixin\",\n    \"braid.GameRendererAccessor\",\n    \"braid.GuiRendererAccessor\",\n    \"braid.GuiRendererMixin\",\n    \"braid.KeyboardHandlerMixin\",\n    \"braid.Matrix3x2fStackAccessor\",\n    \"braid.ScreenMixin\",\n    \"braid.ToastManagerMixin\",\n    \"braid.LevelRendererMixin\",\n    \"itemgroup.CreativeModeInventoryScreenAccessor\",\n    \"itemgroup.CreativeModeInventoryScreenMixin\",\n    \"itemgroup.MixinCreativeModeInventoryScreenMixin\",\n    \"itemgroup.EffectsInInventoryMixin\",\n    \"shader.GlProgramAccessor\",\n    \"text.ClientLanguageMixin\",\n    \"tweaks.OperatingSystemMixin\",\n    \"tweaks.EditBoxMixin\",\n    \"ui.ChatScreenMixin\",\n    \"ui.AbstractWidgetMixin\",\n    \"ui.CubeMapMixin\",\n    \"ui.MultiLineEditBoxMixin\",\n    \"ui.GuiRendererMixin\",\n    \"ui.AbstractContainerScreenMixin\",\n    \"ui.MinecraftMixin\",\n    \"ui.ScreenMixin\",\n    \"ui.AbstractSliderButtonMixin\",\n    \"ui.EditBoxMixin\",\n    \"ui.access.BaseOwoHandledScreenAccessor\",\n    \"ui.access.ButtonAccessor\",\n    \"ui.access.CheckboxAccessor\",\n    \"ui.access.AbstractWidgetAccessor\",\n    \"ui.access.GuiGraphicsAccessor\",\n    \"ui.access.MultilineTextFieldAccessor\",\n    \"ui.access.MultiLineEditBoxAccessor\",\n    \"ui.access.EntityRendererAccessor\",\n    \"ui.access.GlCommandEncoderAccessor\",\n    \"ui.access.RenderSystemAccessor\",\n    \"ui.access.TextBoxComponentAccessor\",\n    \"ui.access.EditBoxAccessor\",\n    \"ui.display.GameRendererMixin\",\n    \"ui.display.GuiMixin\",\n    \"ui.display.MinecraftMixin\",\n    \"ui.display.MouseHandlerMixin\",\n    \"ui.layers.AbstractContainerScreenAccessor\",\n    \"ui.layers.KeyboardHandlerMixin\",\n    \"ui.layers.MouseHandlerMixin\",\n    \"ui.layers.ScreenMixin\"\n  ],\n  \"server\": [\n    \"MainMixin\"\n  ],\n  \"injectors\": {\n    \"defaultRequire\": 1\n  }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/BraidSamplesItem.java",
    "content": "package io.wispforest.owo.samples.braid;\n\nimport io.wispforest.owo.braid.core.BraidScreen;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.Navigator;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.IntrinsicWidth;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.owo.braid.widgets.button.MessageButton;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.InteractionResult;\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.util.Util;\nimport net.minecraft.world.level.Level;\n\nimport java.util.List;\n\npublic class BraidSamplesItem extends Item {\n\n    public BraidSamplesItem(Properties settings) {\n        super(settings);\n    }\n\n    @Override\n    @Environment(EnvType.CLIENT)\n    public InteractionResult use(Level level, Player user, InteractionHand hand) {\n        if (!level.isClientSide()) {\n            return InteractionResult.SUCCESS;\n        }\n\n        Minecraft.getInstance().setScreen(new BraidScreen(SCREEN_SETTINGS, new SampleSelector()));\n        return InteractionResult.SUCCESS;\n    }\n\n    // ---\n\n    private static final BraidScreen.Settings SCREEN_SETTINGS = Util.make(() -> {\n        var settings = new BraidScreen.Settings();\n        settings.shouldPause = false;\n\n        return settings;\n    });\n\n    private static List<Sample> allSamples() {\n        return List.of(\n            new Sample(new SimpleCounter(), \"Simple Counter\"),\n            new Sample(new SharedCounter(), \"Shared Counter\"),\n            new Sample(new LayoutWidgetExamples(), \"Layout Widget Examples\")\n        );\n    }\n\n    public record Sample(Widget widget, String name) {}\n\n    // ---\n\n    public static class SampleSelector extends StatefulWidget {\n        @Override\n        public WidgetState<SampleSelector> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<SampleSelector> {\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Center(\n                    new Panel(\n                        Panel.VANILLA_LIGHT,\n                        new Padding(\n                            Insets.all(6),\n                            new Panel(\n                                Panel.VANILLA_INSET,\n                                new Padding(\n                                    Insets.all(4),\n                                    new IntrinsicWidth(\n                                        new Column(\n                                            MainAxisAlignment.CENTER,\n                                            CrossAxisAlignment.CENTER,\n                                            new Padding(Insets.vertical(2)),\n                                            allSamples().stream()\n                                                .map(sample -> new MessageButton(\n                                                    Component.literal(sample.name()),\n                                                    () -> Navigator.push(context, sample.widget())\n                                                ))\n                                                .toList()\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/LayoutWidgetExamples.java",
    "content": "package io.wispforest.owo.samples.braid;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.grid.Grid;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.scroll.ScrollAnimationSettings;\nimport io.wispforest.owo.braid.widgets.scroll.VerticallyScrollable;\nimport io.wispforest.owo.samples.braid.layout.*;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.util.Mth;\n\nimport java.util.List;\n\npublic class LayoutWidgetExamples extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        var examples = List.of(\n            new Example(\n                \"Padding\",\n                new PaddedLogo()\n            ),\n            new Example(\n                \"Grow and align\",\n                new BottomRightLogo()\n            ),\n            new Example(\n                \"Align with size factors\",\n                new SizeFactorLogo()\n            ),\n            new Example(\n                \"Sized\",\n                new SquishedLogo()\n            ),\n            new Example(\n                \"Constrain\",\n                new LargeLogo()\n            ),\n            new Example(\n                \"Vertical Flex\",\n                new VerticalFlex()\n            ),\n            new Example(\n                \"Row\",\n                new NormalRow()\n            ),\n            new Example(\n                \"Column\",\n                new PaddedColumn()\n            ),\n            new Example(\n                \"Grid\",\n                new Checkerboard()\n            ),\n            new Example(\n                \"Stack\",\n                new RGBStack()\n            ),\n            new Example(\n                \"Stack with base\",\n                new LavaLogo()\n            )\n        );\n\n        return new LayoutBuilder(\n            (builderContext, constraints) -> {\n                var crossAxisCells = Mth.floor(constraints.maxWidth() / 150);\n                return new VerticallyScrollable(\n                    null, ScrollAnimationSettings.DEFAULT,\n                    new Align(\n                        Alignment.TOP,\n                        new Grid(\n                            LayoutAxis.VERTICAL,\n                            crossAxisCells,\n                            Grid.CellFit.loose(),\n                            widget -> new Padding(\n                                Insets.all(5),\n                                new Box(\n                                    Color.BLACK.withA(.25),\n                                    new Padding(\n                                        Insets.all(5),\n                                        new AspectRatio(\n                                            1,\n                                            widget\n                                        )\n                                    )\n                                )\n                            ),\n                            examples\n                        )\n                    )\n                );\n            }\n        );\n    }\n\n    public static class Example extends StatelessWidget {\n\n        public final String name;\n        public final Widget child;\n\n        public Example(String name, Widget child) {\n            this.name = name;\n            this.child = child;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Column(\n                new Flexible(\n                    new Center(\n                        this.child\n                    )\n                ),\n                new Label(LabelStyle.SHADOW, true, Component.literal(this.name))\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/SharedCounter.java",
    "content": "package io.wispforest.owo.samples.braid;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.button.MessageButton;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.Flexible;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.sharedstate.ShareableState;\nimport io.wispforest.owo.braid.widgets.sharedstate.SharedState;\nimport net.minecraft.network.chat.Component;\n\npublic class SharedCounter extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Center(\n            new Panel(\n                Panel.VANILLA_DARK,\n                new Padding(\n                    Insets.all(10),\n                    new Sized(\n                        70,\n                        null,\n                        new SharedState<>(\n                            CounterState::new,\n                            new Column(\n                                new Row(\n                                    new Flexible(\n                                        new CountButton(-1)\n                                    ),\n                                    new Flexible(\n                                        new CountButton(1)\n                                    )\n                                ),\n                                new Padding(\n                                    Insets.top(5),\n                                    new CountLabel()\n                                )\n                            )\n                        )\n                    )\n                )\n            )\n        );\n    }\n\n    public static class CountLabel extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            var count = SharedState.get(context, CounterState.class).count;\n            return Label.literal(\"Count: \" + count);\n        }\n    }\n\n    public static class CountButton extends StatelessWidget {\n\n        public final int countBy;\n\n        public CountButton(int countBy) {\n            this.countBy = countBy;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new MessageButton(\n                this.countBy > 0 ? Component.literal(\"+\" + this.countBy) : Component.literal(String.valueOf(this.countBy)),\n                () -> SharedState.set(context, CounterState.class, state -> state.count += this.countBy)\n            );\n        }\n    }\n\n    public static class CounterState extends ShareableState {\n        public int count = 0;\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/SimpleCounter.java",
    "content": "package io.wispforest.owo.samples.braid;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.owo.braid.widgets.button.MessageButton;\nimport net.minecraft.network.chat.Component;\n\npublic class SimpleCounter extends StatefulWidget {\n    @Override\n    public WidgetState<SimpleCounter> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<SimpleCounter> {\n\n        private int count = 0;\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Center(\n                new Panel(\n                    Panel.VANILLA_LIGHT,\n                    new Padding(\n                        Insets.all(10),\n                        new MessageButton(\n                            Component.literal(\"Count: \" + this.count),\n                            () -> this.setState(() -> {\n                                this.count++;\n                            })\n                        )\n                    )\n                )\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/BottomRightLogo.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.BraidLogo;\nimport io.wispforest.owo.braid.widgets.basic.Align;\n\npublic class BottomRightLogo extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        // move the logo to the bottom right of the\n        // available space\n        return new Align(\n            Alignment.BOTTOM_RIGHT,\n            new BraidLogo()\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/Checkerboard.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.grid.Grid;\n\npublic class Checkerboard extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Center(\n            new Sized(                     // since the boxes we'll be using are unsized,\n                40.0, 60.0,         // we set the size of the grid explicitly\n                new Grid(\n                    LayoutAxis.VERTICAL,  // main axis is vertical:\n                    2,                    // thus, 2 cross-axis cells means two columns\n\n                    Grid.CellFit.tight(), // we force all children to fill their cells\n                                          // and since the grid is tightly constrained, each\n                                          // cell will be exactly 20x20 pixels\n\n                    new Box(Color.WHITE), // arrange some white and black boxes such that\n                    new Box(Color.BLACK), // they create a checkerboard\n                    new Box(Color.BLACK),\n                    new Box(Color.WHITE),\n                    new Box(Color.WHITE),\n                    new Box(Color.BLACK)\n                )\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/LargeLogo.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.Constraints;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.BraidLogo;\nimport io.wispforest.owo.braid.widgets.basic.Constrain;\n\npublic class LargeLogo extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Constrain(\n            Constraints.only(\n                128.0, // minimum width\n                128.0, // minimum height\n                null, // no maximum width\n                null // no maximum height\n            ),\n            new BraidLogo()\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/LavaLogo.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.BraidLogo;\nimport io.wispforest.owo.braid.widgets.SpriteWidget;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport net.minecraft.client.renderer.texture.TextureAtlas;\nimport net.minecraft.client.resources.model.Material;\nimport net.minecraft.resources.Identifier;\n\npublic class LavaLogo extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Stack(\n            new SpriteWidget(\n                new Material(\n                    TextureAtlas.LOCATION_BLOCKS,\n                    Identifier.withDefaultNamespace(\"block/lava_flow\") // the lava flow sprite is 32x32, which is smaller than\n                )                                                // the 64x64 braid logo\n            ),\n            new StackBase(                                       // but by making the logo the base, the lava will be\n                new BraidLogo()                                  // force to have the same size, effectively using it\n            )                                                    // as a backdrop for the logo\n        );\n    }\n}"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/NormalRow.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.label.Label;\n\npublic class NormalRow extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Row(                     // main axis is always horizontal in a row\n                                            // by not specifying alignment for either axis,\n                                            // we default to start alignment on both\n            Label.literal(\"child 1\"),\n            Label.literal(\"child 2\")\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/PaddedColumn.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.label.Label;\n\nimport java.util.List;\n\npublic class PaddedColumn extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Column(                     // main axis is always vertical in a row\n            MainAxisAlignment.CENTER,          // by setting the alignment to center on both axis we'll\n            CrossAxisAlignment.CENTER,         // achieve the same thing a Center widget would\n\n            new Padding(Insets.all(2)),  // we specify a separator (just padding in this case) to\n                                               // space the children out nicely\n            List.of(\n                Label.literal(\"child 1\"), // when using a separator, we must specify the children\n                Label.literal(\"child 2\")  // in a List to differentiate the method signature and\n            )                                  // tell braid which widget should be the separator\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/PaddedLogo.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.BraidLogo;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Padding;\n\npublic class PaddedLogo extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Box(\n            Color.WHITE,             // white backdrop to visualize the padding\n            new Padding(\n                Insets.all(5), // insert 5 pixels of padding on all sides\n                new Box(\n                    Color.BLACK,     // black background to visualize the widget\n                    new BraidLogo()  // bounds\n                )\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/RGBStack.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Size;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Center;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\n\npublic class RGBStack extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Center(\n            new Stack(\n                Alignment.BOTTOM_RIGHT,\n                new Sized(\n                    Size.square(60),\n                    new Box(Color.RED)\n                ),\n                new Sized(\n                    Size.square(40),\n                    new Box(Color.GREEN)\n                ),\n                new Sized(\n                    Size.square(20),\n                    new Box(Color.BLUE)\n                )\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/SizeFactorLogo.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.BraidLogo;\nimport io.wispforest.owo.braid.widgets.basic.Align;\nimport io.wispforest.owo.braid.widgets.basic.Box;\n\npublic class SizeFactorLogo extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Box(\n            Color.WHITE,            // white backdrop to visualize align size\n            new Align(\n                Alignment.TOP_LEFT,\n                1.5,\n                1.5,\n                new Box(\n                    Color.BLACK,    // black backdrop to visualize widget bounds\n                    new BraidLogo()\n                )\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/SquishedLogo.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.BraidLogo;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\n\npublic class SquishedLogo extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        // squish the braid logo on the vertical axis\n        return new Sized(\n            null,\n            32, // the braid logo is normally 64x64, so this\n            // makes it half that size vertically\n            new BraidLogo()\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/owo/samples/braid/layout/VerticalFlex.java",
    "content": "package io.wispforest.owo.samples.braid.layout;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Flex;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.label.Label;\n\npublic class VerticalFlex extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Sized(                   // since our example environment has loose constraints\n                                            // and we want to see a long main axis, force it\n                                            // to have the maximum possible size\n            null, Double.POSITIVE_INFINITY,\n            new Flex(\n                LayoutAxis.VERTICAL,        // main axis is vertical\n                MainAxisAlignment.END,      // align to the end of the main axis, i.e. the bottom\n                CrossAxisAlignment.STRETCH, // stretch the cross axis to its maximum possible size\n                                            // and force the children to fill it\n                Label.literal(\"child 1\"),\n                Label.literal(\"child 2\")\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/EpicMenu.java",
    "content": "package io.wispforest.uwu;\n\nimport io.wispforest.owo.client.screens.MenuUtils;\nimport io.wispforest.owo.client.screens.SlotGenerator;\nimport io.wispforest.owo.client.screens.SyncedProperty;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.SimpleContainer;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.inventory.ContainerLevelAccess;\nimport net.minecraft.world.inventory.ClickType;\nimport net.minecraft.network.chat.Component;\n\nimport java.util.concurrent.ThreadLocalRandom;\n\npublic class EpicMenu extends AbstractContainerMenu {\n    private static final char[] VOWELS = {'a', 'e', 'i', 'o', 'u'};\n    private static final char[] CONSONANTS = {'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'};\n\n    private final ContainerLevelAccess context;\n\n    public final SyncedProperty<String> epicNumber;\n    public final SyncedProperty<ItemStack> cringeStack;\n\n    public EpicMenu(int syncId, Inventory inventory) {\n        this(syncId, inventory, ContainerLevelAccess.NULL);\n    }\n\n    public EpicMenu(int syncId, Inventory inventory, ContainerLevelAccess context) {\n        super(Uwu.EPIC_SCREEN_HANDLER_TYPE, syncId);\n        this.context = context;\n        SlotGenerator.begin(this::addSlot, 8, 84)\n                .grid(new SimpleContainer(4), 0, 4, 1)\n                .playerInventory(inventory);\n\n        this.epicNumber = this.createProperty(String.class, \"\");\n        this.epicNumber.set(generateEpicName());\n\n        this.cringeStack = this.createProperty(ItemStack.class, ItemStack.EMPTY);\n\n        this.addClientboundMessage(MaldMessage.class, this::handleMald);\n        this.addServerboundMessage(EpicMessage.class, this::handleEpic);\n    }\n\n    private void handleMald(MaldMessage r) {\n        this.sendMessage(new EpicMessage(r.number));\n    }\n\n    private void handleEpic(EpicMessage r) {\n        this.epicNumber.set(generateEpicName() + \" \" + r.number);\n\n        var stacc = Items.FEATHER.getDefaultInstance();\n        stacc.set(DataComponents.CUSTOM_NAME, Component.literal(this.epicNumber.get()));\n        this.cringeStack.set(stacc);\n    }\n\n    @Override\n    public void clicked(int slotIndex, int button, ClickType actionType, Player player) {\n        if (!player.level().isClientSide())\n            this.sendMessage(new MaldMessage(slotIndex));\n\n        super.clicked(slotIndex, button, actionType, player);\n    }\n\n    // made originally by det hoonter tm\n    private static String generateEpicName() {\n        var sb = new StringBuilder();\n\n        for (int i = 0; i < 4; ++i) {\n            var consonant = CONSONANTS[ThreadLocalRandom.current().nextInt(CONSONANTS.length)];\n\n            if (i == 0) consonant = Character.toUpperCase(consonant);\n\n            sb.append(consonant);\n            sb.append(VOWELS[ThreadLocalRandom.current().nextInt(VOWELS.length)]);\n        }\n\n        return sb.toString();\n    }\n\n    @Override\n    public ItemStack quickMoveStack(Player player, int index) {\n        return MenuUtils.handleSlotTransfer(this, index, 4);\n    }\n\n    @Override\n    public boolean stillValid(Player player) {\n        return true;\n    }\n\n    public record EpicMessage(int number) {}\n\n    public record MaldMessage(int number) {}\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/FabledBananasClass.java",
    "content": "package io.wispforest.uwu;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.format.gson.GsonDeserializer;\nimport io.wispforest.endec.format.gson.GsonSerializer;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.core.BlockPos;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class FabledBananasClass {\n\n    public static final Endec<FabledBananasClass> ENDEC = StructEndecBuilder.of(\n            Endec.INT.fieldOf(\"banana_amount\", FabledBananasClass::bananaAmount),\n            MinecraftEndecs.ofRegistry(BuiltInRegistries.ITEM).fieldOf(\"banana_item\", FabledBananasClass::bananaItem),\n            MinecraftEndecs.BLOCK_POS.listOf().fieldOf(\"banana_positions\", FabledBananasClass::bananaPositions),\n            FabledBananasClass::new\n    );\n\n    private final int bananaAmount;\n    private final Item bananaItem;\n    private final List<BlockPos> bananaPositions;\n\n    public FabledBananasClass(int bananaAmount, Item bananaItem, List<BlockPos> bananaPositions) {\n        this.bananaAmount = bananaAmount;\n        this.bananaItem = bananaItem;\n        this.bananaPositions = bananaPositions;\n    }\n\n    public int bananaAmount() {return this.bananaAmount;}\n    public Item bananaItem() {return this.bananaItem;}\n    public List<BlockPos> bananaPositions() {return this.bananaPositions;}\n\n    public static void main(String[] args) {\n        var pos = new BlockPos(1, 2, 3);\n        JsonElement result = MinecraftEndecs.BLOCK_POS.encodeFully(GsonSerializer::of, pos);\n\n        System.out.println(result);\n        BlockPos decoded = MinecraftEndecs.BLOCK_POS.decodeFully(GsonDeserializer::of, result);\n\n\n        Endec<Map<Identifier, Integer>> endec = Endec.map(Identifier::toString, Identifier::parse, Endec.INT);\n        System.out.println(endec.encodeFully(GsonSerializer::of, Map.of(Identifier.parse(\"a\"), 6, Identifier.parse(\"b\"), 9)).toString());\n        System.out.println(endec.decodeFully(GsonDeserializer::of, new Gson().fromJson(\"{\\\"a:b\\\":24,\\\"c\\\":17}\", JsonObject.class)));\n\n        Endec<Map<BlockPos, Identifier>> mappy = Endec.map(MinecraftEndecs.BLOCK_POS, MinecraftEndecs.IDENTIFIER);\n        System.out.println(mappy.encodeFully(GsonSerializer::of, Map.of(BlockPos.ZERO, Identifier.parse(\"a\"), new BlockPos(69, 420, 489), Identifier.parse(\"bruh:l\"))).toString());\n        System.out.println(mappy.decodeFully(GsonDeserializer::of, new Gson().fromJson(\"[{\\\"k\\\":[69,420,489],\\\"v\\\":\\\"bruh:l\\\"},{\\\"k\\\":[0,0,0],\\\"v\\\":\\\"minecraft:a\\\"}]\", JsonArray.class)));\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/Uwu.java",
    "content": "package io.wispforest.uwu;\n\nimport com.google.common.collect.ImmutableList;\nimport com.google.gson.Gson;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.mojang.brigadier.arguments.StringArgumentType;\nimport com.mojang.logging.LogUtils;\nimport io.netty.buffer.Unpooled;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.format.bytebuf.ByteBufSerializer;\nimport io.wispforest.endec.format.gson.GsonDeserializer;\nimport io.wispforest.endec.format.gson.GsonSerializer;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.config.ConfigSynchronizer;\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.itemgroup.Icon;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.itemgroup.gui.ItemGroupButton;\nimport io.wispforest.owo.network.OwoNetChannel;\nimport io.wispforest.owo.particles.ClientParticles;\nimport io.wispforest.owo.particles.systems.ParticleSystem;\nimport io.wispforest.owo.particles.systems.ParticleSystemController;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.RegistriesAttribute;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.owo.serialization.format.nbt.NbtDeserializer;\nimport io.wispforest.owo.serialization.format.nbt.NbtSerializer;\nimport io.wispforest.owo.text.CustomTextRegistry;\nimport io.wispforest.owo.util.TagInjector;\nimport io.wispforest.uwu.block.BraidDisplayBlock;\nimport io.wispforest.uwu.block.BraidDisplayBlockEntity;\nimport io.wispforest.uwu.config.BruhConfig;\nimport io.wispforest.uwu.config.UwuConfig;\nimport io.wispforest.uwu.items.UwuItems;\nimport io.wispforest.uwu.network.*;\nimport io.wispforest.uwu.text.BasedTextContent;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.ModInitializer;\nimport net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;\nimport net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup;\nimport net.fabricmc.fabric.api.networking.v1.PacketByteBufs;\nimport net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.world.item.*;\nimport net.minecraft.world.level.block.state.BlockBehaviour;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.Blocks;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.nbt.NbtOps;\nimport net.minecraft.network.RegistryFriendlyByteBuf;\nimport net.minecraft.core.particles.PowerParticleOption;\nimport net.minecraft.core.particles.ParticleTypes;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.tags.BlockTags;\nimport net.minecraft.tags.ItemTags;\nimport net.minecraft.tags.TagKey;\nimport net.minecraft.world.flag.FeatureFlags;\nimport net.minecraft.world.inventory.MenuType;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.Direction;\nimport org.slf4j.Logger;\n\nimport java.util.*;\nimport java.util.function.Consumer;\n\nimport static net.minecraft.commands.Commands.argument;\nimport static net.minecraft.commands.Commands.literal;\n\npublic class Uwu implements ModInitializer {\n\n    private static final Logger LOGGER = LogUtils.getLogger();\n\n    public static final boolean WE_TESTEN_HANDSHAKE = false;\n\n    public static final TagKey<Item> TAB_2_CONTENT = TagKey.create(Registries.ITEM, Identifier.fromNamespaceAndPath(\"uwu\", \"tab_2_content\"));\n    public static final Identifier GROUP_TEXTURE = Identifier.fromNamespaceAndPath(\"uwu\", \"textures/gui/group.png\");\n    public static final Identifier OWO_ICON_TEXTURE = Identifier.fromNamespaceAndPath(\"uwu\", \"textures/gui/icon.png\");\n    public static final Identifier ANIMATED_BUTTON_TEXTURE = Identifier.fromNamespaceAndPath(\"uwu\", \"textures/gui/animated_icon_test.png\");\n\n    public static final MenuType<EpicMenu> EPIC_SCREEN_HANDLER_TYPE = Registry.register(\n        BuiltInRegistries.MENU,\n        Identifier.fromNamespaceAndPath(\"uwu\", \"epic_screen_handler\"),\n        new MenuType<>(EpicMenu::new, FeatureFlags.VANILLA_SET)\n    );\n\n    public static final Block BRAID_DISPLAY_BLOCK = new BraidDisplayBlock(BlockBehaviour.Properties.ofFullCopy(Blocks.IRON_BLOCK).setId(ResourceKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath(\"uwu\", \"braid_display\"))));\n    public static final BlockEntityType<BraidDisplayBlockEntity> BRAID_DISPLAY_ENTITY = FabricBlockEntityTypeBuilder.create(BraidDisplayBlockEntity::new, BRAID_DISPLAY_BLOCK).build();\n\n    public static final OwoItemGroup FOUR_TAB_GROUP = OwoItemGroup.builder(Identifier.fromNamespaceAndPath(\"uwu\", \"four_tab_group\"), () -> Icon.of(Items.AXOLOTL_BUCKET))\n        .disableDynamicTitle()\n        .buttonStackHeight(1)\n        .initializer(group -> {\n            group.addTab(Icon.of(ANIMATED_BUTTON_TEXTURE, 32, 1000, false), \"tab_1\", null, true);\n            group.addTab(Icon.of(Items.EMERALD), \"tab_2\", TAB_2_CONTENT, false);\n            group.addTab(Icon.of(Items.AMETHYST_SHARD), \"tab_3\", null, false);\n            group.addTab(Icon.of(Items.GOLD_INGOT), \"tab_4\", null, false);\n\n            group.addButton(ItemGroupButton.github(group, \"https://github.com/wisp-forest/owo-lib\"));\n        })\n        .build();\n\n    public static final OwoItemGroup SIX_TAB_GROUP = OwoItemGroup.builder(Identifier.fromNamespaceAndPath(\"uwu\", \"six_tab_group\"), () -> Icon.of(Items.POWDER_SNOW_BUCKET))\n        .tabStackHeight(3)\n        .backgroundTexture(GROUP_TEXTURE)\n        .scrollerTextures(new OwoItemGroup.ScrollerTextures(Identifier.fromNamespaceAndPath(\"uwu\", \"scroller\"), Identifier.fromNamespaceAndPath(\"uwu\", \"scroller_disabled\")))\n        .tabTextures(new OwoItemGroup.TabTextures(\n            Identifier.fromNamespaceAndPath(\"uwu\", \"top_selected\"),\n            Identifier.fromNamespaceAndPath(\"uwu\", \"top_selected_first_column\"),\n            Identifier.fromNamespaceAndPath(\"uwu\", \"top_unselected\"),\n            Identifier.fromNamespaceAndPath(\"uwu\", \"bottom_selected\"),\n            Identifier.fromNamespaceAndPath(\"uwu\", \"bottom_selected_first_column\"),\n            Identifier.fromNamespaceAndPath(\"uwu\", \"bottom_unselected\")))\n        .initializer(group -> {\n            group.addTab(Icon.of(Items.DIAMOND), \"tab_1\", null, true);\n            group.addTab(Icon.of(Items.EMERALD), \"tab_2\", null, false);\n            group.addTab(Icon.of(Items.AMETHYST_SHARD), \"tab_3\", null, false);\n            group.addTab(Icon.of(Items.GOLD_INGOT), \"tab_4\", null, false);\n            group.addCustomTab(Icon.of(Items.IRON_INGOT), \"tab_5\", (context, entries) -> {\n                entries.accept(UwuItems.SCREEN_SHARD);\n                entries.accept(UwuItems.BRAID);\n                entries.accept(BRAID_DISPLAY_BLOCK);\n            }, false);\n            group.addTab(Icon.of(Items.QUARTZ), \"tab_6\", null, false);\n\n            group.addButton(new ItemGroupButton(group, Icon.of(OWO_ICON_TEXTURE, 0, 0, 16, 16), Owo.MOD_ID, () -> {\n                Minecraft.getInstance().player.displayClientMessage(Component.nullToEmpty(\"oωo button pressed!\"), false);\n            }));\n        })\n        .build();\n\n    public static final OwoItemGroup SINGLE_TAB_GROUP = OwoItemGroup.builder(Identifier.fromNamespaceAndPath(\"uwu\", \"single_tab_group\"), () -> Icon.of(OWO_ICON_TEXTURE, 0, 0, 16, 16))\n        .displaySingleTab()\n        .initializer(group -> group.addTab(Icon.of(Items.SPONGE), \"tab_1\", null, true))\n        .build();\n\n    public static final CreativeModeTab VANILLA_GROUP = Registry.register(BuiltInRegistries.CREATIVE_MODE_TAB, Identifier.fromNamespaceAndPath(\"uwu\", \"vanilla_group\"), FabricItemGroup.builder()\n        .title(Component.literal(\"who did this\"))\n        .icon(Items.ACACIA_BOAT::getDefaultInstance)\n        .displayItems((context, entries) -> entries.accept(Items.MANGROVE_CHEST_BOAT))\n        .build());\n\n    public static final OwoNetChannel CHANNEL = OwoNetChannel.create(Identifier.fromNamespaceAndPath(\"uwu\", \"uwu\"));\n\n    public static final TestMessage MESSAGE = new TestMessage(\"hahayes\", 69, Long.MAX_VALUE, ItemStack.EMPTY, Short.MAX_VALUE, Byte.MAX_VALUE, new BlockPos(69, 420, 489),\n        Float.NEGATIVE_INFINITY, Double.NaN, false, Identifier.fromNamespaceAndPath(\"uowou\", \"hahayes\"), Collections.emptyMap(),\n        new int[]{10, 20}, new String[]{\"trollface\"}, new short[]{1, 2, 3}, new long[]{Long.MAX_VALUE, 1, 3}, new byte[]{1, 2, 3, 4},\n        Optional.of(\"NullableString\"), Optional.empty(),\n        ImmutableList.of(new BlockPos(9786, 42, 9234)), new SealedSubclassOne(\"basede\", 10), new SealedSubclassTwo(10, null));\n\n    public static final ParticleSystemController PARTICLE_CONTROLLER = new ParticleSystemController(Identifier.fromNamespaceAndPath(\"uwu\", \"particles\"));\n    public static final ParticleSystem<Void> CUBE = PARTICLE_CONTROLLER.registerDeferred(Void.class);\n    public static final ParticleSystem<Void> BREAK_BLOCK_PARTICLES = PARTICLE_CONTROLLER.register(Void.class, (world, pos, data) -> {\n        ClientParticles.persist();\n\n        ClientParticles.setParticleCount(30);\n        ClientParticles.spawnLine(PowerParticleOption.create(ParticleTypes.DRAGON_BREATH, 1), world, pos.add(.5, .5, .5), pos.add(.5, 2.5, .5), .015f);\n\n        ClientParticles.randomizeVelocityOnAxis(.1, Direction.Axis.Z);\n        ClientParticles.spawn(ParticleTypes.CLOUD, world, pos.add(.5, 2.5, .5), 0);\n\n        ClientParticles.reset();\n    });\n\n    public static final UwuConfig CONFIG = UwuConfig.createAndLoad();\n    public static final BruhConfig BRUHHHHH = BruhConfig.createAndLoad(builder -> {\n//        builder.janksonBuilder().registerSerializer(Color.class, (color, marshaller) -> new JsonPrimitive(\"bruv\"));\n    });\n\n    @Override\n    public void onInitialize() {\n\n        var stackEndec = CodecUtils.toEndec(ItemStack.CODEC);\n        var stackData = \"\"\"\n                    {\n                        \"id\": \"minecraft:shroomlight\",\n                        \"Count\": 42,\n                        \"tag\": {\n                            \"Enchantments\": [{\"id\": \"unbreaking\", \"lvl\": 3}]\n                        }\n                    }\n            \"\"\";\n\n        var stacknite = stackEndec.decode(SerializationContext.empty(), GsonDeserializer.of(new Gson().fromJson(stackData, JsonObject.class)));\n        System.out.println(stacknite);\n\n        var serializer = ByteBufSerializer.of(PacketByteBufs.create());\n        stackEndec.encode(SerializationContext.empty(), serializer, stacknite);\n\n        System.out.println(serializer.result().read(SerializationContext.empty(), stackEndec));\n        System.out.println(CodecUtils.toCodec(MinecraftEndecs.BLOCK_POS).encodeStart(NbtOps.INSTANCE, new BlockPos(34, 35, 69)).result().get());\n\n        UwuItems.init();\n\n        TagInjector.inject(BuiltInRegistries.BLOCK, BlockTags.BASE_STONE_OVERWORLD.location(), Blocks.GLASS);\n        TagInjector.injectTagReference(BuiltInRegistries.ITEM, ItemTags.COALS.location(), ItemTags.FOX_FOOD.location());\n\n        FOUR_TAB_GROUP.initialize();\n        SIX_TAB_GROUP.initialize();\n        SINGLE_TAB_GROUP.initialize();\n\n        CHANNEL.registerClientbound(TestMessage.class, (message, access) -> {\n            access.player().displayClientMessage(Component.nullToEmpty(message.string), false);\n        });\n\n        CHANNEL.registerClientboundDeferred(OtherTestMessage.class);\n\n        CHANNEL.registerServerbound(TestMessage.class, (message, access) -> {\n            access.player().displayClientMessage(Component.nullToEmpty(String.valueOf(message.bite)), false);\n            access.player().displayClientMessage(Component.nullToEmpty(String.valueOf(message)), false);\n        });\n\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER && WE_TESTEN_HANDSHAKE) {\n            OwoNetChannel.create(Identifier.fromNamespaceAndPath(\"uwu\", \"server_only_channel\"));\n            new ParticleSystemController(Identifier.fromNamespaceAndPath(\"uwu\", \"server_only_particles\"));\n        }\n\n        System.out.println(BuiltInRegistries.ITEM.wrapAsHolder(Items.ACACIA_BOAT));\n        System.out.println(BuiltInRegistries.ITEM.get(Identifier.parse(\"acacia_planks\")));\n\n        Registry.register(BuiltInRegistries.BLOCK, Identifier.fromNamespaceAndPath(\"uwu\", \"braid_display\"), BRAID_DISPLAY_BLOCK);\n        Registry.register(BuiltInRegistries.ITEM, Identifier.fromNamespaceAndPath(\"uwu\", \"braid_display\"), new BlockItem(BRAID_DISPLAY_BLOCK, new Item.Properties().setId(ResourceKey.create(Registries.ITEM, Identifier.fromNamespaceAndPath(\"uwu\", \"braid_display\"))).useBlockDescriptionPrefix()));\n        Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, Identifier.fromNamespaceAndPath(\"uwu\", \"braid_display\"), BRAID_DISPLAY_ENTITY);\n\n//        UwuShapedRecipe.init();\n\n        CommandRegistrationCallback.EVENT.register((dispatcher, access, environment) -> {\n\n            dispatcher.register(literal(\"get_option\")\n                .then(argument(\"config\", StringArgumentType.string())\n                    .then(argument(\"option\", StringArgumentType.string()).executes(context -> {\n                        var value = ConfigSynchronizer.getClientOptions(\n                            context.getSource().getPlayer(),\n                            StringArgumentType.getString(context, \"config\")\n                        ).get(new Option.Key(StringArgumentType.getString(context, \"option\")));\n\n                        context.getSource().sendSuccess(() -> Component.literal(String.valueOf(value)), false);\n\n                        return 0;\n                    }))));\n\n            dispatcher.register(literal(\"kodeck_test\")\n                .executes(context -> {\n                    var rand = context.getSource().getLevel().random;\n                    var source = context.getSource();\n\n                    //--\n\n                    String testPhrase = \"This is a test to see how kodeck dose.\";\n\n                    LOGGER.info(\"Input:  \" + testPhrase);\n\n                    var nbtData = Endec.STRING.encodeFully(NbtSerializer::of, testPhrase);\n                    var fromNbtData = Endec.STRING.decodeFully(NbtDeserializer::of, nbtData);\n\n                    var jsonData = Endec.STRING.encodeFully(GsonSerializer::of, fromNbtData);\n                    var fromJsonData = Endec.STRING.decodeFully(GsonDeserializer::of, jsonData);\n\n                    LOGGER.info(\"Output: \" + fromJsonData);\n\n                    LOGGER.info(\"\");\n\n                    //--\n\n                    int randomNumber = rand.nextInt(20000);\n\n                    LOGGER.info(\"Input:  \" + randomNumber);\n\n                    var jsonNum = Endec.INT.encodeFully(GsonSerializer::of, randomNumber);\n\n                    LOGGER.info(\"Output: \" + Endec.INT.decodeFully(GsonDeserializer::of, jsonNum));\n\n                    LOGGER.info(\"\");\n\n                    //--\n\n                    List<Integer> randomNumbers = new ArrayList<>();\n\n                    var maxCount = rand.nextInt(20);\n\n                    for (int i = 0; i < maxCount; i++) {\n                        randomNumbers.add(rand.nextInt(20000));\n                    }\n\n                    LOGGER.info(\"Input:  \" + randomNumbers);\n\n                    Endec<List<Integer>> INT_LIST_KODECK = Endec.INT.listOf();\n\n                    var nbtListData = INT_LIST_KODECK.encodeFully(NbtSerializer::of, randomNumbers);\n\n                    LOGGER.info(\"Output: \" + INT_LIST_KODECK.decodeFully(NbtDeserializer::of, nbtListData));\n\n                    LOGGER.info(\"\");\n\n                    //---\n\n                    if (source.getPlayer() == null) return 0;\n\n                    ItemStack handStack = source.getPlayer().getItemInHand(InteractionHand.MAIN_HAND);\n\n                    LOGGER.info(handStack.toString());\n                    LOGGER.info(handStack.getComponents().toString().replace(\"\\n\", \"\\\\n\"));\n\n                    LOGGER.info(\"---\");\n\n                    JsonElement stackJsonData;\n\n                    try {\n                        stackJsonData = MinecraftEndecs.ITEM_STACK.encodeFully(SerializationContext.attributes(RegistriesAttribute.of(context.getSource().getLevel().registryAccess())), GsonSerializer::of, handStack);\n                    } catch (Exception exception) {\n                        LOGGER.info(exception.getMessage());\n                        LOGGER.info((Arrays.toString(exception.getStackTrace())));\n\n                        return 0;\n                    }\n\n                    LOGGER.info(stackJsonData.toString());\n\n                    LOGGER.info(\"---\");\n\n                    try {\n                        handStack = MinecraftEndecs.ITEM_STACK.decodeFully(SerializationContext.attributes(RegistriesAttribute.of(context.getSource().getLevel().registryAccess())), GsonDeserializer::of, stackJsonData);\n                    } catch (Exception exception) {\n                        LOGGER.info(exception.getMessage());\n                        LOGGER.info((Arrays.toString(exception.getStackTrace())));\n\n                        return 0;\n                    }\n\n                    LOGGER.info(handStack.toString());\n                    LOGGER.info(handStack.getComponents().toString().replace(\"\\n\", \"\\\\n\"));\n\n                    LOGGER.info(\"\");\n\n                    //--\n\n                    // TODO: kodeck test\n//{\n//                            LOGGER.info(\"--- Format Based Endec Test\");\n//\n//                            var nbtDataStack = handStack.toNbt(access);\n//\n//                            LOGGER.info(\"  Input:  \" + nbtDataStack.asString().get().replace(\"\\n\", \"\\\\n\"));\n//\n//                            var jsonDataStack = NbtEndec.ELEMENT.encodeFully(GsonSerializer::of, nbtDataStack);\n//\n//                            LOGGER.info(\"  Json:  \" + jsonDataStack);\n//\n//                            var convertedNbtDataStack = NbtEndec.ELEMENT.decodeFully(GsonDeserializer::of, jsonDataStack);\n//\n//                            LOGGER.info(\"Output:  \" + convertedNbtDataStack.asString().get().replace(\"\\n\", \"\\\\n\"));\n//\n//                            LOGGER.info(\"---\");\n//\n//                        LOGGER.info(\"\");\n//                        }\n//\n                    ////--\n//\n//                    {\n//                            LOGGER.info(\"--- Transpose Format Based Endec Test\");\n//\n//                            var nbtDataStack = handStack.toNbt(access);\n//\n//                            LOGGER.info(\"  Input:  \" + nbtDataStack.asString().get().replace(\"\\n\", \"\\\\n\"));\n//\n//                            var jsonDataStack = NbtEndec.ELEMENT.encodeFully(GsonSerializer::of, nbtDataStack);\n//\n//                            LOGGER.info(\"  Json:  \" + jsonDataStack);\n//\n//                            var convertedNbtDataStack = GsonEndec.INSTANCE.encodeFully(NbtSerializer::of, jsonDataStack);\n//\n//                            LOGGER.info(\"Output:  \" + convertedNbtDataStack.asString().get().replace(\"\\n\", \"\\\\n\"));\n//\n//                            LOGGER.info(\"---\");\n//\n//                        LOGGER.info(\"\");\n//                        }\n\n                    //--\n\n                    {\n                        var variable1Endec = Endec.STRING.keyed(\"variable1\", \"\");\n                        var variable2Endec = Endec.INT.keyed(\"variable2\", 0);\n                        var variable3Endec = TestRecord.ENDEC.keyed(\"variable3Endec\", (TestRecord) null);\n\n                        var variable1 = \"Weeeeeee\";\n                        var variable2 = 1000;\n                        var variable3 = new TestRecord(\"Matt\", 24, List.of(\"One\", \"Two\", \"Three\", \"Four\"));\n\n                        LOGGER.info(variable1);\n                        LOGGER.info(String.valueOf(variable2));\n                        LOGGER.info(String.valueOf(variable3));\n\n                        CompoundTag compound = new CompoundTag();\n\n                        compound.put(variable1Endec, variable1);\n                        compound.put(variable2Endec, variable2);\n                        compound.put(variable3Endec, variable3);\n\n                        LOGGER.info(\"\");\n                        LOGGER.info(compound.toString());\n\n                        LOGGER.info(\"\");\n\n                        LOGGER.info(compound.get(variable1Endec));\n                        LOGGER.info(compound.get(variable2Endec).toString());\n                        LOGGER.info(compound.get(variable3Endec).toString());\n\n                        LOGGER.info(\"---\");\n                        LOGGER.info(\"\");\n                    }\n\n                    //--\n\n\n                    //--\n\n                    var stack = !source.getPlayer().getItemInHand(InteractionHand.MAIN_HAND).isEmpty()\n                        ? source.getPlayer().getItemInHand(InteractionHand.MAIN_HAND)\n                        : Items.SHULKER_BOX.getDefaultInstance();\n\n                    //Vanilla\n                    iterations(\"Vanilla\", (buf) -> {\n                        ItemStack.STREAM_CODEC.encode(buf, stack);\n                        var stackFromByte = ItemStack.STREAM_CODEC.decode(buf);\n                    });\n\n                    //Codeck\n                    try {\n                        var ctx = SerializationContext.attributes(RegistriesAttribute.of(context.getSource().getLevel().registryAccess()));\n                        iterations(\"Endec\", (buf) -> {\n                            buf.write(ctx, MinecraftEndecs.ITEM_STACK, stack);\n\n                            var stackFromByte = buf.read(ctx, MinecraftEndecs.ITEM_STACK);\n                        });\n                    } catch (Exception exception) {\n                        LOGGER.info(exception.getMessage());\n                        LOGGER.info(Arrays.toString(exception.getStackTrace()));\n\n                        return 0;\n                    }\n\n                    return 0;\n                }));\n        });\n\n        CustomTextRegistry.register(\"based\", BasedTextContent.CODEC);\n\n        UwuNetworkExample.init();\n        UwuOptionalNetExample.init();\n    }\n\n    private static void iterations(String label, Consumer<RegistryFriendlyByteBuf> action) {\n        int maxTrials = 3;\n        int maxIterations = 500;\n\n        List<Long> durations = new ArrayList<>();\n\n        LOGGER.info(\"-----\");\n        LOGGER.info(label);\n\n        for (int trial = 0; trial < maxTrials; trial++) {\n            durations.clear();\n\n            for (int i = 0; i < maxIterations; i++) {\n                RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), Owo.currentServer().registryAccess());\n\n                long startTime = System.nanoTime();\n\n                action.accept(buf);\n\n                durations.add(System.nanoTime() - startTime);\n            }\n\n            LOGGER.info(String.format(maxIterations + \" Trials took on average: %.2f\", ((durations.stream().mapToLong(v -> v).sum()) / (double) durations.size()) / 1000000));\n        }\n\n        LOGGER.info(\"-----\");\n\n    }\n\n    public record TestRecord(String name, int count, List<String> names) {\n        public static final Endec<TestRecord> ENDEC = StructEndecBuilder.of(\n            Endec.STRING.fieldOf(\"name\", TestRecord::name),\n            Endec.INT.fieldOf(\"count\", TestRecord::count),\n            Endec.STRING.listOf().fieldOf(\"names\", TestRecord::names),\n            TestRecord::new\n        );\n    }\n\n    public record OtherTestMessage(BlockPos pos, String message) {}\n\n    public record TestMessage(String string, Integer integer, Long along, ItemStack stack, Short ashort, Byte bite,\n                              BlockPos pos, Float afloat, Double adouble, Boolean aboolean, Identifier identifier,\n                              Map<String, Integer> map,\n                              int[] arr1, String[] arr2, short[] arr3, long[] arr4, byte[] arr5,\n                              Optional<String> optional1, Optional<String> optional2,\n                              List<BlockPos> posses, SealedTestClass sealed1, SealedTestClass sealed2) {}\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/block/BraidDisplayBlock.java",
    "content": "package io.wispforest.uwu.block;\n\nimport com.mojang.serialization.MapCodec;\nimport io.wispforest.owo.braid.core.KeyModifiers;\nimport io.wispforest.owo.braid.core.events.KeyPressEvent;\nimport net.minecraft.world.level.block.Block;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.block.BaseEntityBlock;\nimport net.minecraft.world.phys.shapes.CollisionContext;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.InteractionResult;\nimport net.minecraft.world.phys.BlockHitResult;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.world.phys.shapes.VoxelShape;\nimport net.minecraft.world.level.BlockGetter;\nimport net.minecraft.world.level.Level;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\npublic class BraidDisplayBlock extends BaseEntityBlock {\n\n    public static final VoxelShape SHAPE = Block.box(\n        0, 0, 0, 16, 2, 16\n    );\n\n    public BraidDisplayBlock(Properties settings) {\n        super(settings);\n    }\n\n    @Override\n    protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {\n        var entity = level.getBlockEntity(pos);\n        if (player.isShiftKeyDown() && entity instanceof BraidDisplayBlockEntity display && display.display != null) {\n            display.display.app.eventBinding.add(new KeyPressEvent(GLFW.GLFW_KEY_I, GLFW.glfwGetKeyScancode(GLFW.GLFW_KEY_I), new KeyModifiers(GLFW.GLFW_MOD_SHIFT | GLFW.GLFW_MOD_CONTROL)));\n\n            return InteractionResult.SUCCESS;\n        }\n\n        return InteractionResult.PASS;\n    }\n\n    @Override\n    protected VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) {\n        return SHAPE;\n    }\n\n    @Override\n    public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) {\n        return new BraidDisplayBlockEntity(pos, state);\n    }\n\n    @Override\n    protected MapCodec<? extends BaseEntityBlock> codec() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/block/BraidDisplayBlockEntity.java",
    "content": "package io.wispforest.uwu.block;\n\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.display.BraidDisplay;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport io.wispforest.owo.braid.display.DisplayQuad;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.InheritedWidget;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.object.BlockWidget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.Button;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.slider.slider.MessageSlider;\nimport io.wispforest.uwu.Uwu;\nimport io.wispforest.uwu.items.UwuItems;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.block.Blocks;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.world.phys.Vec3;\n\nimport java.lang.ref.WeakReference;\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class BraidDisplayBlockEntity extends BlockEntity {\n\n    @Environment(EnvType.CLIENT)\n    public BraidDisplay display;\n    @Environment(EnvType.CLIENT)\n    public AtomicBoolean disposed;\n\n    // ---\n\n    public BraidDisplayBlockEntity(BlockPos pos, BlockState state) {\n        super(Uwu.BRAID_DISPLAY_ENTITY, pos, state);\n    }\n\n    public static BraidDisplayBlockEntity of(BuildContext context) {\n        return Objects.requireNonNull(context.getAncestor(Provider.class)).display.get();\n    }\n\n    @Override\n    @Environment(EnvType.CLIENT)\n    public void setRemoved() {\n        super.setRemoved();\n\n        if (this.disposed != null) {\n            if (!this.disposed.compareAndSet(false, true)) return;\n            this.display.app.dispose();\n            BraidDisplayBinding.deactivate(this.display);\n        }\n    }\n\n    // ---\n\n    public static class App extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            return new Center(\n                new Column(\n                    MainAxisAlignment.START,\n                    CrossAxisAlignment.CENTER,\n                    new Padding(Insets.vertical(4)),\n                    List.of(\n                        new Sized(\n                            112,\n                            20,\n                            new BlockSlider()\n                        ),\n                        new Panel(\n                            Panel.VANILLA_DARK,\n                            new Padding(\n                                Insets.all(10),\n                                new Label(Component.translatable(\"text.uwu.braid\").append(Component.literal(\" on block real??\")))\n                            )\n                        ),\n                        new Button(\n                            () -> Minecraft.getInstance().player.handleCreativeModeItemDrop(UwuItems.BRAID.getDefaultInstance()),\n                            new Row(\n                                MainAxisAlignment.START,\n                                CrossAxisAlignment.CENTER,\n                                new Sized(\n                                    16,\n                                    16,\n                                    new BlockWidget(Blocks.OBSERVER.defaultBlockState())\n                                ),\n                                new Padding(Insets.horizontal(2)),\n                                new Label(\n                                    LabelStyle.SHADOW,\n                                    true,\n                                    Component.translatable(\"text.uwu.braid\").append(Component.literal(\" button\"))\n                                )\n                            )\n                        )\n                    )\n                )\n            );\n        }\n\n        public static class BlockSlider extends StatefulWidget {\n            @Override\n            public WidgetState<BlockSlider> createState() {\n                return new State();\n            }\n\n            public static class State extends WidgetState<BlockSlider> {\n\n                private double value = 1;\n\n                @Override\n                public Widget build(BuildContext context) {\n                    return new MessageSlider(\n                        this.value,\n                        Component.literal(\"size: \" + BigDecimal.valueOf(this.value).setScale(2, RoundingMode.HALF_UP).toPlainString()), slider -> slider\n                            .range(1, 3),\n                        (newValue) -> {\n                            this.setState(() -> this.value = newValue);\n\n                            var display =BraidDisplayBlockEntity.of(context).display;\n\n                            display.quad = new DisplayQuad(\n                                display.quad.pos,\n                                new Vec3(0, 0, -14 / 16d),\n                                new Vec3(14 / 16d + (this.value - 1), 0, 0)\n                            );\n\n                            display.surface.resize(128, (int) (146.29 * display.quad.left.x));\n                        }\n                    );\n                }\n            }\n        }\n    }\n\n    public static class Provider extends InheritedWidget {\n\n        public final WeakReference<BraidDisplayBlockEntity> display;\n\n        public Provider(BraidDisplayBlockEntity display, Widget child) {\n            super(child);\n            this.display = new WeakReference<>(display);\n        }\n\n        @Override\n        public boolean mustRebuildDependents(InheritedWidget newWidget) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/blockentity/ProcessBlockEntity.java",
    "content": "package io.wispforest.uwu.blockentity;\n\nimport io.wispforest.owo.blockentity.LinearProcess;\nimport io.wispforest.owo.blockentity.LinearProcessExecutor;\nimport net.minecraft.world.level.block.state.BlockState;\nimport net.minecraft.world.level.block.entity.BlockEntity;\nimport net.minecraft.world.level.block.entity.BlockEntityType;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.world.level.Level;\n\npublic class ProcessBlockEntity extends BlockEntity {\n\n    public static final LinearProcess<ProcessBlockEntity> PROCESS = new LinearProcess<>(30);\n\n    private final LinearProcessExecutor<ProcessBlockEntity> executor;\n\n    public ProcessBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {\n        super(type, pos, state);\n        this.executor = PROCESS.createExecutor(this);\n    }\n\n    @Override\n    public void setLevel(Level world) {\n        super.setLevel(world);\n        PROCESS.configureExecutor(this.executor, world.isClientSide());\n    }\n\n    public void tick() {\n        this.executor.tick();\n    }\n\n    static {\n\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/Bikeshed.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.basic.TextureWidget;\nimport net.minecraft.resources.Identifier;\n\npublic class Bikeshed extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Sized(\n            256,\n            256,\n            new TextureWidget(\n                Identifier.fromNamespaceAndPath(\"uwu\", \"textures/gui/bikeshed.png\"),\n                TextureWidget.Wrap.STRETCH, Color.WHITE\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/BraidDisplayBlockEntityRenderer.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.braid.display.BraidDisplay;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport io.wispforest.owo.braid.display.DisplayQuad;\nimport io.wispforest.owo.braid.widgets.basic.Panel;\nimport io.wispforest.uwu.block.BraidDisplayBlockEntity;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.renderer.blockentity.BlockEntityRenderer;\nimport net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;\nimport net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;\nimport net.minecraft.client.renderer.feature.ModelFeatureRenderer;\nimport net.minecraft.client.renderer.SubmitNodeCollector;\nimport net.minecraft.client.renderer.state.CameraRenderState;\nimport com.mojang.blaze3d.vertex.PoseStack;\nimport net.minecraft.world.phys.Vec3;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.lang.ref.Cleaner;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class BraidDisplayBlockEntityRenderer implements BlockEntityRenderer<BraidDisplayBlockEntity, BraidDisplayBlockEntityRenderer.BraidDisplayBlockEntityRenderState> {\n\n    public BraidDisplayBlockEntityRenderer(BlockEntityRendererProvider.Context ctx) {}\n\n    @Override\n    public BraidDisplayBlockEntityRenderState createRenderState() {\n        return new BraidDisplayBlockEntityRenderState();\n    }\n\n    @Override\n    public void extractRenderState(BraidDisplayBlockEntity entity, BraidDisplayBlockEntityRenderState state, float tickProgress, Vec3 cameraPos, @Nullable ModelFeatureRenderer.CrumblingOverlay crumblingOverlay) {\n        BlockEntityRenderer.super.extractRenderState(entity, state, tickProgress, cameraPos, crumblingOverlay);\n\n        if (entity.display == null) {\n            entity.disposed = new AtomicBoolean();\n            entity.display = new BraidDisplay(\n                new DisplayQuad(\n                    Vec3.atLowerCornerOf(entity.getBlockPos()).add(1 / 16d, 2 / 16d + 1e-5, 1 - 1 / 16d),\n                    new Vec3(0, 0, -14 / 16d),\n                    new Vec3(14 / 16d, 0, 0)\n                ),\n                128, 128,\n                new BraidDisplayBlockEntity.Provider(\n                    entity,\n                    new Panel(\n                        Panel.VANILLA_LIGHT,\n                        new BraidDisplayBlockEntity.App()\n                    )\n                )\n            );\n\n            BraidDisplayBinding.activate(entity.display);\n            DISPLAY_CLEANER.register(entity, new DisplayCleanCallback(entity.display, entity.disposed));\n        }\n\n        state.display = entity.display;\n    }\n\n    @Override\n    public void submit(BraidDisplayBlockEntityRenderState state, PoseStack matrices, SubmitNodeCollector queue, CameraRenderState cameraState) {\n        var display = state.display;\n        if (display.surface.texture() == null) return;\n\n        matrices.translate(\n            display.quad.pos.subtract(Vec3.atLowerCornerOf(state.blockPos)).add(0, 1e-4, 0)\n        );\n\n        display.render(matrices, queue, state.lightCoords);\n    }\n\n    public static class BraidDisplayBlockEntityRenderState extends BlockEntityRenderState {\n        public BraidDisplay display;\n    }\n\n    // ---\n\n    private static final Cleaner DISPLAY_CLEANER = Cleaner.create();\n\n    private record DisplayCleanCallback(BraidDisplay display, AtomicBoolean disposed) implements Runnable {\n        @Override\n        public void run() {\n            if (!this.disposed.compareAndSet(false, true)) return;\n\n            Minecraft.getInstance().schedule(() -> {\n                this.display.app.dispose();\n                BraidDisplayBinding.deactivate(this.display);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/ComponentTestScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport com.mojang.authlib.GameProfile;\nimport io.wispforest.owo.ui.component.*;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.container.ScrollContainer;\nimport io.wispforest.owo.ui.core.*;\nimport net.minecraft.world.level.block.Blocks;\nimport net.minecraft.world.level.block.FurnaceBlock;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.client.gui.GuiGraphics;\nimport net.minecraft.client.gui.components.events.GuiEventListener;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.components.Tooltip;\nimport net.minecraft.client.input.KeyEvent;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.world.item.component.BundleContents;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.network.chat.ClickEvent;\nimport net.minecraft.network.chat.HoverEvent;\nimport net.minecraft.network.chat.FontDescription;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.util.CommonColors;\nimport org.jetbrains.annotations.Nullable;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.UUID;\nimport java.util.stream.IntStream;\n\npublic class ComponentTestScreen extends Screen {\n\n    private OwoUIAdapter<FlowLayout> uiAdapter = null;\n//    private RenderEffectWrapper<?>.RenderEffectSlot fadeSlot = null;\n\n    public ComponentTestScreen() {\n        super(Component.empty());\n    }\n\n    @Override\n    protected void init() {\n        this.uiAdapter = OwoUIAdapter.create(this, UIContainers::horizontalFlow);\n        final var rootComponent = uiAdapter.rootComponent;\n\n        rootComponent.child(\n                UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                        .child(UIComponents.button(Component.nullToEmpty(\"Dark Background\"), button -> rootComponent.surface(Surface.flat(0x77000000))).horizontalSizing(Sizing.fixed(95)))\n                        .child(UIComponents.button(Component.nullToEmpty(\"No Background\"), button -> rootComponent.surface(Surface.BLANK)).margins(Insets.vertical(5)).horizontalSizing(Sizing.fixed(95)))\n                        .child(UIComponents.button(Component.nullToEmpty(\"Dirt Background\"), button -> rootComponent.surface(Surface.optionsBackground())).horizontalSizing(Sizing.fixed(95)))\n                        .child(UIComponents.checkbox(Component.nullToEmpty(\"bruh\")).onChanged(aBoolean -> this.minecraft.player.displayClientMessage(Component.nullToEmpty(\"bruh: \" + aBoolean), false)).margins(Insets.top(5)))\n                        .padding(Insets.of(10))\n                        .surface(Surface.vanillaPanorama(true))\n                        .positioning(Positioning.relative(1, 1))\n        );\n\n        final var innerLayout = UIContainers.verticalFlow(Sizing.content(100), Sizing.content());\n        var verticalAnimation = innerLayout.verticalSizing().animate(350, Easing.SINE, Sizing.content(50));\n\n        verticalAnimation.finished().subscribe((direction, looping) -> {\n            minecraft.gui.getChat().addMessage(Component.literal(\"vertical animation finished in direction \" + direction.name()));\n        });\n\n        final var bruh = UIComponents.box(Sizing.fixed(150), Sizing.fixed(20));\n        bruh.horizontalSizing().animate(5000, Easing.QUARTIC, Sizing.fixed(10)).forwards();\n        innerLayout.child(bruh);\n\n        final var otherBox = UIContainers.verticalFlow(Sizing.fixed(150), Sizing.fixed(20));\n        otherBox.surface(Surface.flat(Color.BLACK.argb())).horizontalSizing().animate(5000, Easing.QUARTIC, Sizing.fixed(10)).forwards();\n        innerLayout.child(otherBox);\n\n        innerLayout.child(UIContainers.verticalScroll(Sizing.content(), Sizing.fixed(50), UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                                .child(new BoxComponent(Sizing.fixed(20), Sizing.fixed(40)).margins(Insets.of(5)))\n                                .child(new BoxComponent(Sizing.fixed(45), Sizing.fixed(45)).margins(Insets.of(5)))\n                                .child(UIComponents.textBox(Sizing.fixed(60)))\n                                .horizontalAlignment(HorizontalAlignment.RIGHT)\n                                .surface(Surface.flat(0x77000000)))\n                        .scrollbar(ScrollContainer.Scrollbar.vanilla())\n                        .fixedScrollbarLength(15)\n                        .scrollbarThiccness(12)\n                        .id(\"scrollnite\")\n                )\n                .child(UIComponents.button(Component.nullToEmpty(\"+\"), (ButtonComponent button) -> {\n                            verticalAnimation.reverse();\n\n                            button.setMessage(verticalAnimation.direction() == Animation.Direction.FORWARDS\n                                    ? Component.nullToEmpty(\"-\")\n                                    : Component.nullToEmpty(\"+\")\n                            );\n                        }).<ButtonComponent>configure(button -> {\n                            button.setTooltip(Tooltip.create(Component.nullToEmpty(\"a vanilla tooltip\")));\n                            button.margins(Insets.of(5)).sizing(Sizing.fixed(12));\n                        })\n                )\n                .child(new BoxComponent(Sizing.fixed(40), Sizing.fixed(20)).margins(Insets.of(5)))\n                .horizontalAlignment(HorizontalAlignment.CENTER)\n                .verticalAlignment(VerticalAlignment.CENTER)\n                .padding(Insets.of(5));\n\n        innerLayout.child(UIComponents.textArea(Sizing.fixed(75), Sizing.content()).maxLines(5).displayCharCount(true));\n        innerLayout.child(UIComponents.textArea(Sizing.fixed(75), Sizing.fixed(75)).<TextAreaComponent>configure(textArea -> {\n            textArea.displayCharCount(true).setCharacterLimit(100);\n        }));\n\n        rootComponent.child(UIContainers.horizontalScroll(Sizing.fill(20), Sizing.content(), innerLayout)\n                .scrollbarThiccness(6)\n                .scrollbar(ScrollContainer.Scrollbar.vanillaFlat())\n                .surface(Surface.DARK_PANEL)\n                .padding(Insets.of(3))\n        );\n\n        rootComponent.child(UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                .child(UIComponents.label(Component.literal(\"A profound vertical Flow Layout, as well as a leally long text to demonstrate wrapping\").withStyle(style -> style.withFont(new FontDescription.Resource(Minecraft.UNIFORM_FONT)))\n                                .withStyle(style -> {\n                                    return style.withClickEvent(new ClickEvent.CopyToClipboard(\"yes\"))\n                                            .withHoverEvent(new HoverEvent.ShowItem(Items.SCULK_SHRIEKER.getDefaultInstance()));\n                                }))\n                        .shadow(true)\n                        .lineHeight(7)\n                        .lineSpacing(0)\n                        .maxWidth(100)\n                        .margins(Insets.horizontal(15)))\n        );\n\n        final var buttonPanel = UIContainers.horizontalFlow(Sizing.content(), Sizing.content())\n                .child(UIComponents.label(Component.literal(\"AAAAAAAAAAAAAAAAAAA\").append(Component.literal(\"Layout\")\n                                .withStyle(style -> style.withHoverEvent(new HoverEvent.ShowItem(Items.SCULK_SHRIEKER.getDefaultInstance()))))\n                        .append(Component.literal(\"\\nAAAAAAAAAAAAAAA\"))).margins(Insets.of(5)))\n                .child(UIComponents.button(Component.nullToEmpty(\"⇄\"), button -> this.rebuildWidgets()).sizing(Sizing.fixed(20)))\n                .child(UIComponents.button(Component.nullToEmpty(\"X\"), button -> this.onClose()).sizing(Sizing.fixed(20)))\n                .positioning(Positioning.relative(100, 0))\n                .verticalAlignment(VerticalAlignment.CENTER)\n                .surface(Surface.TOOLTIP)\n                .padding(Insets.of(5))\n                .margins(Insets.of(10));\n\n        final var growingTextBox = UIComponents.textBox(Sizing.fixed(60));\n        final var growAnimation = growingTextBox.horizontalSizing().animate(500, Easing.SINE, Sizing.fixed(80));\n        growingTextBox.mouseEnter().subscribe(growAnimation::forwards);\n        growingTextBox.mouseLeave().subscribe(growAnimation::backwards);\n\n        var weeAnimation = buttonPanel.positioning().animate(1000, Easing.CUBIC, Positioning.relative(0, 100));\n        rootComponent.child(UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                .child(growingTextBox)\n                .child(new SmallCheckboxComponent())\n                .child(UIComponents.textBox(Sizing.fixed(60)))\n                .child(UIComponents.button(Component.nullToEmpty(\"weeeee\"), button -> {\n                    weeAnimation.loop(!weeAnimation.looping());\n                    rootComponent.<FlowLayout>configure(layout -> {\n                        var padding = layout.padding().get();\n                        for (int i = 0; i < 696969; i++) {\n                            layout.padding(Insets.of(i));\n                        }\n                        layout.padding(padding.add(5, 5, 5, 5));\n                    });\n                }).renderer(ButtonComponent.Renderer.flat(0x77000000, 0x77070707, 0xA0000000)).sizing(Sizing.content()))\n                .child(UIComponents.discreteSlider(Sizing.fill(10), 0, 5).<DiscreteSliderComponent>configure(\n                        slider -> slider.snap(true)\n                                .decimalPlaces(1)\n                                .message(value -> Component.translatable(\"text.ui.test_slider\", value))\n                                .onChanged().subscribe(value -> {\n                                    slider.parent().surface(Surface.blur(3, (float) (value * 3)));\n                                    this.minecraft.player.displayClientMessage(Component.nullToEmpty(\"sliding towards \" + value), false);\n                                })\n                ))\n                .gap(10)\n                .padding(Insets.both(5, 10))\n                .horizontalAlignment(HorizontalAlignment.CENTER)\n                .surface(Surface.blur(3, 0))\n        );\n\n        var dropdown = UIComponents.dropdown(Sizing.content())\n                .checkbox(Component.nullToEmpty(\"more checking\"), true, aBoolean -> {})\n                .text(Component.nullToEmpty(\"hahayes\"))\n                .button(Component.nullToEmpty(\"epic button\"), dropdownComponent -> {})\n                .divider()\n                .text(Component.nullToEmpty(\"very good\"))\n                .checkbox(Component.nullToEmpty(\"checking time\"), false, aBoolean -> {})\n                .nested(Component.nullToEmpty(\"nested entry\"), Sizing.content(), nested -> {\n                    nested.text(Component.nullToEmpty(\"nest title\"))\n                            .divider()\n                            .button(Component.nullToEmpty(\"nest button\"), dropdownComponent -> {});\n                });\n\n        var dropdownButton = UIComponents.button(Component.nullToEmpty(\"Dropdown\"), button -> {\n            if (dropdown.hasParent()) return;\n            rootComponent.child(dropdown.positioning(Positioning.absolute(button.x(), button.y() + button.height())));\n        }).margins(Insets.horizontal(8));\n        dropdown.mouseLeave().subscribe(() -> dropdown.closeWhenNotHovered(true));\n\n//        rootComponent.child(\n//                Containers.renderEffect(\n//                        Containers.verticalFlow(Sizing.content(), Sizing.content())\n//                                .child(Containers.renderEffect(\n//                                        Components.sprite(new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, Identifier.of(\"block/stone\"))).margins(Insets.of(5))\n//                                ).<RenderEffectWrapper<?>>configure(wrapper -> {\n//                                    wrapper.effect(RenderEffectWrapper.RenderEffect.rotate(RotationAxis.POSITIVE_Z, -45));\n//                                    wrapper.effect(RenderEffectWrapper.RenderEffect.color(Color.ofHsv(.5f, 1f, 1f)));\n//                                }))\n//                                .child(dropdownButton)\n//                ).<RenderEffectWrapper<?>>configure(wrapper -> {\n//                    wrapper.effect(RenderEffectWrapper.RenderEffect.transform(matrices -> matrices.translate(0, 25, 0)));\n//\n//                    wrapper.effect(RenderEffectWrapper.RenderEffect.rotate(90f));\n//                    this.fadeSlot = wrapper.effect(RenderEffectWrapper.RenderEffect.color(Color.WHITE));\n//                })\n//        );\n\n        rootComponent.mouseDown().subscribe((click, doubled) -> {\n            if (click.button() != GLFW.GLFW_MOUSE_BUTTON_RIGHT) return false;\n            DropdownComponent.openContextMenu(this, rootComponent, FlowLayout::child, click.x(), click.y(), contextMenu -> {\n                contextMenu.text(Component.literal(\"That's a context menu\"));\n                contextMenu.checkbox(Component.literal(\"Yup\"), true, aBoolean -> {});\n                contextMenu.divider();\n                contextMenu.button(Component.literal(\"Delet\"), UIComponent::remove);\n            });\n            return true;\n        });\n\n//        rootComponent.child(\n//                new BaseComponent() {\n//                    @Override\n//                    public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) {\n//                        context.drawCircle(\n//                                this.x + this.width / 2,\n//                                this.y + this.height / 2,\n//                                75,\n//                                this.width / 2f,\n//                                Color.ofArgb(0x99000000)\n//                        );\n//\n//                        context.drawRing(\n//                                this.x + this.width / 2,\n//                                this.y + this.height / 2,\n//                                75,\n//                                (this.width - 125) / 2f,\n//                                this.width / 2f,\n//                                Color.ofArgb(0x99000000),\n//                                Color.ofArgb(0x99000000)\n//                        );\n//\n//                        var time = (System.currentTimeMillis() / 1000d) % (Math.PI * 2);\n//                        context.drawLine(\n//                                (int) (this.x + this.width / 2 + Math.cos(time) * this.width / 2),\n//                                (int) (this.y + this.height / 2 + Math.sin(time) * this.height / 2),\n//                                (int) (this.x + this.width / 2 + Math.sin(time) * this.width / 2),\n//                                (int) (this.y + this.height / 2 + Math.cos(time) * this.height / 2),\n//                                1,\n//                                Color.BLUE\n//                        );\n//\n//                        context.drawSpectrum(this.x, this.y, this.width, (int) (this.height * (Math.sin(time) * .5 + .5)), true);\n//                    }\n//                }.positioning(Positioning.relative(50, 50)).sizing(Sizing.fixed(350)));\n        rootComponent.child(\n                UIComponents.button(Component.nullToEmpty(\"overlay\"), button -> {\n                    rootComponent.child(UIContainers.overlay(\n                            UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                                    .child(new ColorPickerComponent()\n                                            .showAlpha(true)\n                                            .selectedColor(Color.ofArgb(0x7F3955E5))\n                                            .sizing(Sizing.fixed(160), Sizing.fixed(100))\n                                    ).padding(Insets.of(5)).surface(Surface.DARK_PANEL)\n                    ));\n                })\n        );\n\n\n        // i knew it all along, chyz truly is a pig\n        var pig = EntityComponent.createRenderablePlayer(new GameProfile(UUID.fromString(\"09de8a6d-86bf-4c15-bb93-ce3384ce4e96\"), \"chyzman\"));\n        pig.setSharedFlagOnFire(true);\n\n        rootComponent.child(\n                UIComponents.entity(Sizing.fixed(100), pig)\n                        .allowMouseRotation(true)\n                        .scaleToFit(true)\n                        .showNametag(true)\n                    .lookAtCursor(true)\n        );\n\n        rootComponent.child(\n                UIComponents.block(Blocks.FURNACE.defaultBlockState().setValue(FurnaceBlock.LIT, true), (CompoundTag) null).sizing(Sizing.fixed(100))\n        );\n\n        var bundle = Items.BUNDLE.getDefaultInstance();\n        var itemList = new ArrayList<ItemStack>();\n        itemList.add(new ItemStack(Items.EMERALD, 16));\n\n        bundle.set(DataComponents.BUNDLE_CONTENTS, new BundleContents(itemList));\n\n        rootComponent.child(UIComponents.item(new ItemStack(Items.EMERALD, 16))\n                .showOverlay(true)\n                .setTooltipFromStack(true)\n                .positioning(Positioning.absolute(120, 30))\n        );\n\n        final var buttonGrid = UIContainers.grid(Sizing.content(), Sizing.fixed(85), 3, 5);\n        for (int row = 0; row < 3; row++) {\n            for (int column = 0; column < 5; column++) {\n                buttonGrid.child(\n                        UIComponents.button(Component.nullToEmpty(\"\" + (row * 5 + column)), button -> {\n                            if (button.getMessage().getString().equals(\"11\")) {\n                                buttonGrid.child(UIComponents.button(Component.nullToEmpty(\"long boiii\"), b -> buttonGrid.child(button, 2, 1)).margins(Insets.of(3)), 2, 1);\n                            } else if (button.getMessage().getString().equals(\"8\")) {\n                                final var box = UIComponents.textBox(Sizing.fill(10));\n                                box.setSuggestion(\"thicc boi\");\n                                box.sizing(box.horizontalSizing().get(), Sizing.fixed(40));\n\n                                buttonGrid.child(box.margins(Insets.of(3)), 1, 3);\n                            }\n                        }).margins(Insets.of(3)).sizing(Sizing.fixed(20)),\n                        row, column\n                );\n            }\n        }\n\n        rootComponent.child(buttonGrid\n                .horizontalAlignment(HorizontalAlignment.CENTER)\n                .verticalAlignment(VerticalAlignment.CENTER)\n                .surface(Surface.PANEL)\n                .padding(Insets.of(4))\n        );\n\n        var data = IntStream.rangeClosed(1, 15).boxed().toList();\n        rootComponent.child(\n                UIContainers.horizontalScroll(\n                                Sizing.fixed(26 * 7 + 8),\n                                Sizing.content(),\n                                UIComponents.list(\n                                        data,\n                                        flowLayout -> flowLayout.margins(Insets.bottom(10)),\n                                        integer -> UIComponents.button(Component.literal(integer.toString()), (ButtonComponent button) -> {}).margins(Insets.horizontal(3)).horizontalSizing(Sizing.fixed(20)),\n                                        false\n                                )\n                        )\n                        .scrollStep(26)\n                        .scrollbarThiccness(7)\n                        .scrollbar(ScrollContainer.Scrollbar.vanilla())\n                        .surface(Surface.PANEL)\n                        .padding(Insets.of(4, 5, 5, 5))\n                        .margins(Insets.bottom(5))\n                        .positioning(Positioning.relative(50, 100))\n        );\n\n        rootComponent.child(\n                UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                        .child(UIComponents.label(Component.literal(\"Cursor Tester\").withColor(CommonColors.GRAY))\n                                .tooltip(Component.literal(\"by chyzman\")))\n                        .child(\n                                UIComponents.list(\n                                        Arrays.stream(CursorStyle.values()).toList(),\n                                        flowLayout -> flowLayout.margins(Insets.bottom(10)),\n                                        cursor -> UIComponents.label(Component.literal(cursor.toString()).withColor(CommonColors.GRAY))\n                                                .cursorStyle(cursor)\n                                                .margins(Insets.horizontal(3)),\n                                        true\n                                )\n                        )\n                        .surface(Surface.PANEL)\n                        .padding(Insets.of(4, 5, 5, 5))\n                        .margins(Insets.bottom(5))\n                        .positioning(Positioning.relative(100, 100))\n        );\n\n        // infinity scroll test\n//        rootComponent.child(\n//                Containers.verticalScroll(Sizing.fixed(243), Sizing.fixed(145),\n//                        Components.box(Sizing.fixed(235), Sizing.fixed(144))\n//                                .startColor(Color.GREEN)\n//                                .endColor(Color.BLUE)\n//                                .direction(BoxComponent.GradientDirection.TOP_TO_BOTTOM)\n//                                .fill(true)\n//                ).padding(Insets.of(4)).positioning(Positioning.absolute(150, 40))\n//        );\n\n        rootComponent.child(buttonPanel);\n        rootComponent.surface(Surface.flat(0x77000000))\n                .verticalAlignment(VerticalAlignment.CENTER)\n                .horizontalAlignment(HorizontalAlignment.CENTER);\n\n        uiAdapter.inflateAndMount();\n    }\n\n    @Override\n    public void renderBackground(GuiGraphics context, int mouseX, int mouseY, float delta) {}\n\n    @Override\n    public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {\n        super.render(context, mouseX, mouseY, delta);\n//        this.fadeSlot.update(RenderEffectWrapper.RenderEffect.color(new Color(\n//                1f, 1f, 1f,\n//                (float) (Math.sin(System.currentTimeMillis() / 1000d) * .5 + .5)\n//        )));\n    }\n\n    @Override\n    public boolean keyPressed(KeyEvent input) {\n        if (input.isEscape()) {\n            this.onClose();\n            return true;\n        }\n\n        if (input.key() == GLFW.GLFW_KEY_F12) {\n            try (var out = Files.newOutputStream(Path.of(\"component_tree.dot\")); var writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {\n                writer.write(\"digraph D {\\n\");\n\n                final var tree = new ArrayList<UIComponent>();\n                this.uiAdapter.rootComponent.collectDescendants(tree);\n\n                for (var component : tree) {\n                    writer.write(\"  \\\"\" + format(component.parent()) + \"\\\" -> \\\"\" + format(component) + \"\\\"\\n\");\n                }\n\n                writer.write(\"}\");\n                writer.flush();\n            } catch (IOException e) {\n                e.printStackTrace();\n            }\n            return true;\n        } else {\n            return this.uiAdapter.keyPressed(input);\n        }\n    }\n\n    @Override\n    public boolean mouseDragged(MouseButtonEvent click, double deltaX, double deltaY) {\n        return this.uiAdapter.mouseDragged(click, deltaX, deltaY);\n    }\n\n    @Override\n    public boolean isPauseScreen() {\n        return false;\n    }\n\n    @Nullable\n    @Override\n    public GuiEventListener getFocused() {\n        return this.uiAdapter;\n    }\n\n    @Override\n    public void removed() {\n        this.uiAdapter.dispose();\n    }\n\n    private String format(@Nullable UIComponent component) {\n        if (component == null) {\n            return \"root\";\n        } else {\n            return component.getClass().getSimpleName() + \"@\" + Integer.toHexString(component.hashCode())\n                    + \"(\" + component.x() + \" \" + component.y() + \")\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/EpicContainerModelScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseUIModelContainerScreen;\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.uwu.EpicMenu;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.network.chat.Component;\n\npublic class EpicContainerModelScreen extends BaseUIModelContainerScreen<FlowLayout, EpicMenu> {\n\n    public EpicContainerModelScreen(EpicMenu handler, Inventory inventory, Component title) {\n        super(handler, inventory, title, FlowLayout.class, BaseUIModelScreen.DataSource.file(\"epic_handled_screen.xml\"));\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        var indexField = rootComponent.childById(EditBox.class, \"index-field\");\n        indexField.setFilter(s -> s.matches(\"\\\\d*\"));\n\n        rootComponent.childById(ButtonComponent.class, \"enable-button\").onPress(button -> this.enableSlot(Integer.parseInt(indexField.getValue())));\n        rootComponent.childById(ButtonComponent.class, \"disable-button\").onPress(button -> this.disableSlot(Integer.parseInt(indexField.getValue())));\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/EpicContainerScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.mixin.ui.SlotAccessor;\nimport io.wispforest.owo.ui.base.BaseOwoContainerScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.component.LabelComponent;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.uwu.EpicMenu;\nimport net.minecraft.client.input.MouseButtonEvent;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.nbt.CompoundTag;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.lwjgl.glfw.GLFW;\n\npublic class EpicContainerScreen extends BaseOwoContainerScreen<FlowLayout, EpicMenu> {\n    private LabelComponent numberLabel;\n\n\n    public EpicContainerScreen(EpicMenu handler, Inventory inventory, Component title) {\n        super(handler, inventory, title);\n    }\n\n    @Override\n    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {\n        return OwoUIAdapter.create(this, UIContainers::verticalFlow);\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        var frogeNbt = new CompoundTag();\n        frogeNbt.putString(\"variant\", \"delightful:froge\");\n\n        var selectBox = UIComponents.textBox(Sizing.fixed(40));\n        selectBox.setFilter(s -> s.matches(\"\\\\d*\"));\n\n        rootComponent.child(\n                UIComponents.texture(Identifier.parse(\"textures/gui/container/shulker_box.png\"), 0, 0, 176, 166)\n        ).child(\n                UIContainers.draggable(\n                        Sizing.content(), Sizing.content(),\n                        UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                                .child(UIComponents.label(Component.literal(\"froge :)\"))\n                                        .horizontalTextAlignment(HorizontalAlignment.CENTER)\n                                        .positioning(Positioning.absolute(0, -9))\n                                        .horizontalSizing(Sizing.fixed(100)))\n                                .child(UIComponents.entity(Sizing.fixed(100), EntityType.FROG, frogeNbt).scale(.75f).allowMouseRotation(true).tooltip(Component.literal(\":)\")))\n                                .child(UIContainers.horizontalFlow(Sizing.fixed(100), Sizing.content())\n                                        .child(UIComponents.button(Component.nullToEmpty(\"✔\"), (ButtonComponent button) -> {\n                                            var text = selectBox.getValue();\n                                            if (text.isBlank()) return;\n                                            try {\n                                                this.enableSlot(Integer.parseInt(text));\n                                            } catch (Exception e) {}\n                                        }).tooltip(Component.literal(\"Enable\")))\n                                        .child(selectBox.margins(Insets.horizontal(3)).tooltip(Component.literal(\"Slot Index\")))\n                                        .child(UIComponents.button(Component.nullToEmpty(\"❌\"), (ButtonComponent button) -> {\n                                            var text = selectBox.getValue();\n                                            if (text.isBlank()) return;\n                                            try {\n                                                this.disableSlot(Integer.parseInt(text));\n                                            } catch (Exception e) {}\n                                        }).tooltip(Component.literal(\"Disable\"))).verticalAlignment(VerticalAlignment.CENTER).horizontalAlignment(HorizontalAlignment.CENTER))\n                                .allowOverflow(true)\n                ).surface(Surface.DARK_PANEL).padding(Insets.of(5)).allowOverflow(true).positioning(Positioning.absolute(100, 100))\n        ).child(\n                UIContainers.verticalScroll(Sizing.content(), Sizing.fill(50), UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                        .child(this.slotAsComponent(0).tooltip(Component.nullToEmpty(\"bruh\")))\n                        .child(UIComponents.box(Sizing.fixed(50), Sizing.fixed(35)).startColor(Color.RED).endColor(Color.BLUE).fill(true).tooltip(Component.literal(\"very very long tooltip\")))\n                        .child(this.slotAsComponent(1))\n                        .child(UIComponents.box(Sizing.fixed(50), Sizing.fixed(35)).startColor(Color.BLUE).endColor(Color.RED).fill(true))\n                        .child(this.slotAsComponent(2))\n                        .child(UIComponents.box(Sizing.fixed(50), Sizing.fixed(35)).startColor(Color.RED).endColor(Color.BLUE).fill(true))\n                        .child(this.slotAsComponent(3))\n                        .child(UIComponents.box(Sizing.fixed(50), Sizing.fixed(35)).startColor(Color.BLUE).endColor(Color.RED).fill(true))\n                ).positioning(Positioning.relative(75, 50)).surface(Surface.outline(0x77000000)).padding(Insets.of(1))\n        ).surface(Surface.VANILLA_TRANSLUCENT).verticalAlignment(VerticalAlignment.CENTER).horizontalAlignment(HorizontalAlignment.CENTER);\n\n        rootComponent.child(\n                (numberLabel = UIComponents.label(Component.literal(menu.epicNumber.get())))\n                        .positioning(Positioning.absolute(0, 0))\n        );\n\n        menu.epicNumber.observe(value -> numberLabel.text(Component.literal(value)));\n    }\n\n    @Override\n    public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {\n        if (click.hasAltDown() && this.hoveredSlot != null) {\n            return false;\n        }\n\n        if (click.button() == GLFW.GLFW_MOUSE_BUTTON_MIDDLE) {\n            this.uiAdapter.rootComponent.child(UIContainers.overlay(UIComponents.label(Component.literal(\"a\"))));\n            return true;\n        }\n\n        return super.mouseClicked(click, doubled);\n    }\n\n    @Override\n    public boolean mouseDragged(MouseButtonEvent click, double deltaX, double deltaY) {\n        if (click.hasAltDown() && this.hoveredSlot != null) {\n            var accessor = ((SlotAccessor) this.hoveredSlot);\n            accessor.owo$setX((int) Math.round(this.hoveredSlot.x + deltaX));\n            accessor.owo$setY((int) Math.round(this.hoveredSlot.y + deltaY));\n        }\n\n        return super.mouseDragged(click, deltaX, deltaY);\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/HudTestWidget.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.core.ListenableValue;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.object.ItemStackWidget;\nimport io.wispforest.uwu.items.UwuItems;\nimport net.minecraft.network.chat.Component;\nimport org.apache.commons.lang3.time.DurationFormatUtils;\n\nimport java.time.Duration;\n\npublic class HudTestWidget extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new ListenableBuilder(\n            SHOW_TEST_HUD,\n            (listenableContext, child) -> new Visibility(\n                SHOW_TEST_HUD.value(),\n                child\n            ),\n            new Align(\n                Alignment.TOP_RIGHT,\n                new Padding(\n                    Insets.all(5),\n                    new Column(\n                        MainAxisAlignment.CENTER,\n                        CrossAxisAlignment.CENTER,\n                        new Sized(\n                            100, null,\n                            new Label(LabelStyle.SHADOW, true, Component.translatable(\"compliance.playtime.message\"))\n                        ),\n                        new Padding(\n                            Insets.all(3),\n                            new ItemStackWidget(UwuItems.BRAID.getDefaultInstance())\n                        ),\n                        new Timer()\n                    )\n                )\n            )\n        );\n    }\n\n    public static class Timer extends StatefulWidget {\n        @Override\n        public WidgetState<Timer> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<Timer> {\n\n            private int seconds = -1;\n\n            @Override\n            public void init() {\n                this.count();\n            }\n\n            private void count() {\n                this.scheduleDelayedCallback(Duration.ofSeconds(1), this::count);\n                this.setState(() -> this.seconds++);\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Label(\n                    LabelStyle.SHADOW,\n                    true,\n                    Component.literal(\"time in session: \" + DurationFormatUtils.formatDuration(this.seconds * 1000L, \"HH:mm:ss\"))\n                );\n            }\n        }\n    }\n\n    // ---\n\n    public static final ListenableValue<Boolean> SHOW_TEST_HUD = new ListenableValue<>(false);\n}"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/LayersTestWidget.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.braid.core.Alignment;\nimport io.wispforest.owo.braid.core.Color;\nimport io.wispforest.owo.braid.core.Insets;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.util.layers.AnchorJustification;\nimport io.wispforest.owo.braid.util.layers.LayerAlignment;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.MessageButton;\nimport io.wispforest.owo.braid.widgets.intents.Interactable;\nimport io.wispforest.owo.braid.widgets.object.ItemStackWidget;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.uwu.items.UwuItems;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.ImageButton;\nimport net.minecraft.network.chat.Component;\n\npublic class LayersTestWidget extends StatelessWidget {\n    @Override\n    public Widget build(BuildContext context) {\n        return new Stack(\n            new Align(\n                Alignment.TOP_LEFT,\n                new Padding(\n                    Insets.all(15),\n                    new MessageButton(\n                        Component.literal(\"layers??\"),\n                        () -> Minecraft.getInstance().getSingleplayerServer().getPlayerList().getPlayers().getFirst().kill(Minecraft.getInstance().getSingleplayerServer().overworld())\n                    )\n                )\n            ),\n            LayerAlignment.atVanillaWidget(\n                clickableWidget -> clickableWidget instanceof ImageButton,\n                AnchorJustification.CENTER_TO_CENTER,\n                new Sized(\n                    10, 10,\n                    new Tooltip(\n                        Component.literal(\"a\"),\n                        Interactable.primary(\n                            () -> Minecraft.getInstance().gui.getChat().addMessage(Component.literal(\"braid layer supremacy\")),\n                            new Box(Color.RED)\n                        )\n                    )\n                )\n            ),\n            LayerAlignment.atContainerScreenCoordinates(\n                136, 63,\n                new ItemStackWidget(UwuItems.BRAID.getDefaultInstance())\n            )\n        );\n    }\n}"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/ParseFailScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport net.minecraft.resources.Identifier;\n\npublic class ParseFailScreen extends BaseUIModelScreen<FlowLayout> {\n\n    public ParseFailScreen() {\n        super(FlowLayout.class, DataSource.asset(Identifier.fromNamespaceAndPath(\"uwu\", \"parse_fail\")));\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n\n    }\n}"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/ScissorTestScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseOwoScreen;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.StackLayout;\nimport io.wispforest.owo.ui.core.*;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.List;\n\npublic class ScissorTestScreen extends BaseOwoScreen<StackLayout> {\n    @Override\n    protected @NotNull OwoUIAdapter<StackLayout> createAdapter() {\n        return OwoUIAdapter.create(this, UIContainers::stack);\n    }\n\n    @Override\n    protected void build(StackLayout rootComponent) {\n        rootComponent.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER);\n        rootComponent.child(UIContainers.verticalScroll(\n            Sizing.fixed(100), Sizing.fixed(35),\n            UIContainers.verticalFlow(Sizing.content(), Sizing.content()).children(List.of(\n                UIComponents.box(Sizing.fixed(75), Sizing.fixed(25)),\n                UIComponents.textBox(Sizing.fill(100)),\n                UIComponents.box(Sizing.fixed(75), Sizing.fixed(25))\n//                Components.textBox(Sizing.fill(100)),\n//                Components.box(Sizing.fixed(75), Sizing.fixed(25))\n            ))\n        ).surface(Surface.VANILLA_TRANSLUCENT));\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/SelectUwuScreenScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.braid.core.BraidScreen;\nimport io.wispforest.owo.config.ui.ConfigScreen;\nimport io.wispforest.owo.ui.base.BaseOwoScreen;\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.uwu.Uwu;\nimport io.wispforest.uwu.client.braid.TestSelector;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.List;\n\npublic class SelectUwuScreenScreen extends BaseOwoScreen<FlowLayout> {\n\n    @Override\n    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {\n        return OwoUIAdapter.create(this, UIContainers::verticalFlow);\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        this.uiAdapter.rootComponent.surface(Surface.flat(0x77000000))\n                .verticalAlignment(VerticalAlignment.CENTER)\n                .horizontalAlignment(HorizontalAlignment.CENTER);\n\n        this.uiAdapter.rootComponent.child(\n                UIComponents.label(Component.literal(\"Available screens\"))\n                        .shadow(true)\n                        .margins(Insets.bottom(5))\n        );\n\n        var panel = UIContainers.horizontalFlow(Sizing.content(), Sizing.content()).<FlowLayout>configure(layout -> {\n            layout.gap(6)\n                    .padding(Insets.of(5))\n                    .surface(Surface.PANEL);\n        });\n\n        var leftColumn = UIContainers.verticalFlow(Sizing.content(), Sizing.content()).gap(6);\n        var rightColumn = UIContainers.verticalFlow(Sizing.content(), Sizing.content()).gap(6);\n\n        panel.children(List.of(leftColumn, rightColumn));\n\n        leftColumn.child(UIComponents.button(Component.literal(\"code demo\"), button -> this.minecraft.setScreen(new ComponentTestScreen())));\n        leftColumn.child(UIComponents.button(Component.literal(\"xml demo\"), button -> this.minecraft.setScreen(new TestParseScreen())));\n        leftColumn.child(UIComponents.button(Component.literal(\"code config\"), button -> this.minecraft.setScreen(new TestConfigScreen())));\n        leftColumn.child(UIComponents.button(Component.literal(\"xml config\"), button -> this.minecraft.setScreen(ConfigScreen.create(Uwu.CONFIG, null))));\n        leftColumn.child(UIComponents.button(Component.literal(\"optimization test\"), button -> this.minecraft.setScreen(new TooManyComponentsScreen())));\n        leftColumn.child(UIComponents.button(Component.literal(\"focus cycle test\"), button -> this.minecraft.setScreen(new BaseUIModelScreen<>(FlowLayout.class, Identifier.fromNamespaceAndPath(\"uwu\", \"focus_cycle_test\")) {\n            @Override\n            protected void build(FlowLayout rootComponent) {}\n        })));\n        leftColumn.child(UIComponents.button(Component.literal(\"expand gap test\"), button -> this.minecraft.setScreen(new BaseUIModelScreen<>(FlowLayout.class, Identifier.fromNamespaceAndPath(\"uwu\", \"expand_gap_test\")) {\n            @Override\n            protected void build(FlowLayout rootComponent) {}\n        })));\n        rightColumn.child(UIComponents.button(Component.literal(\"smolnite\"), button -> this.minecraft.setScreen(new SmolComponentTestScreen())));\n        rightColumn.child(UIComponents.button(Component.literal(\"sizenite\"), button -> this.minecraft.setScreen(new SizingTestScreen())));\n        rightColumn.child(UIComponents.button(Component.literal(\"parse fail\"), button -> this.minecraft.setScreen(new ParseFailScreen())));\n        rightColumn.child(UIComponents.button(Component.literal(\"braid\"), button -> {\n            var settings = new BraidScreen.Settings();\n            settings.shouldPause = false;\n\n            this.minecraft.setScreen(new BraidScreen(settings, new TestSelector()));\n        }));\n        panel.child(UIComponents.button(Component.literal(\"smolnite\"), button -> this.minecraft.setScreen(new SmolComponentTestScreen())));\n        panel.child(UIComponents.button(Component.literal(\"sizenite\"), button -> this.minecraft.setScreen(new SizingTestScreen())));\n        panel.child(UIComponents.button(Component.literal(\"parse fail\"), button -> this.minecraft.setScreen(new ParseFailScreen())));\n        panel.child(UIComponents.button(Component.literal(\"scissor test\"), button -> this.minecraft.setScreen(new ScissorTestScreen())));\n\n        this.uiAdapter.rootComponent.child(panel);\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/SizingTestScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseOwoScreen;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.container.StackLayout;\nimport io.wispforest.owo.ui.core.*;\nimport net.minecraft.network.chat.ClickEvent;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URI;\n\npublic class SizingTestScreen extends BaseOwoScreen<FlowLayout> {\n    @Override\n    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {\n        return OwoUIAdapter.create(this, UIContainers::verticalFlow);\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        rootComponent.horizontalAlignment(HorizontalAlignment.CENTER).verticalAlignment(VerticalAlignment.CENTER);\n        rootComponent.child(UIContainers.stack(Sizing.content(), Sizing.content()).<StackLayout>configure(container -> {\n            container.horizontalAlignment(HorizontalAlignment.CENTER).surface(Surface.panelWithInset(6)).padding(Insets.of(15));\n\n            var animation = container.horizontalSizing().animate(500, Easing.CUBIC, Sizing.fill(75));\n            container.child(UIComponents.button(Component.literal(\"initialize sizenite\"), button -> {\n                animation.reverse();\n            }).horizontalSizing(Sizing.fill(50)));\n        }));\n\n        rootComponent.child(UIComponents.label(Component.literal(\"bruh\").setStyle(Style.EMPTY.withClickEvent(new ClickEvent.OpenUrl(URI.create(\"https://wispforest.io\"))))));\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/SmolComponentTestScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.BoxComponent;\nimport io.wispforest.owo.ui.component.SlimSliderComponent;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.Insets;\nimport io.wispforest.owo.ui.core.Sizing;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\n\npublic class SmolComponentTestScreen extends BaseUIModelScreen<FlowLayout> {\n\n    protected SmolComponentTestScreen() {\n        super(FlowLayout.class, Identifier.fromNamespaceAndPath(\"uwu\", \"smol_components\"));\n    }\n\n    @Override\n    @SuppressWarnings(\"DataFlowIssue\")\n    protected void build(FlowLayout rootComponent) {\n        rootComponent.childById(SlimSliderComponent.class, \"precise-slider\").tooltipSupplier(SlimSliderComponent.valueTooltipSupplier(2));\n\n        rootComponent.childById(SlimSliderComponent.class, \"tiny-steppy-man\").tooltipSupplier(SlimSliderComponent.VALUE_TOOLTIP_SUPPLIER).onChanged().subscribe(value -> {\n            this.minecraft.player.displayClientMessage(Component.literal(\"tiny steppy man: \" + value), false);\n        });\n\n        rootComponent.childById(SlimSliderComponent.class, \"big-steppy-man\").tooltipSupplier(value -> Component.literal(\"big steppy man: \" + value)).onChanged().subscribe(value -> {\n            this.minecraft.player.displayClientMessage(Component.literal(\"big steppy man: \" + value), false);\n        });\n\n        rootComponent.childById(SlimSliderComponent.class, \"inset-slider\").<SlimSliderComponent>configure(slider -> {\n            slider.tooltipSupplier(value -> Component.literal(\"Insets: \" + value.intValue()));\n            slider.onChanged().subscribe(value -> {\n                rootComponent.childById(FlowLayout.class, \"inset-container\").padding(Insets.of((int) value));\n            });\n        });\n\n        this.component(SlimSliderComponent.class, \"expando-slider\").<SlimSliderComponent>configure(slider -> {\n            slider.tooltipSupplier(SlimSliderComponent.VALUE_TOOLTIP_SUPPLIER);\n            slider.onChanged().subscribe(value -> {\n                this.component(BoxComponent.class, \"expando-box\").horizontalSizing(Sizing.fixed((int) value));\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/TestConfigScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.ui.base.BaseOwoScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.core.*;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.HoverEvent;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.stream.IntStream;\n\npublic class TestConfigScreen extends BaseOwoScreen<FlowLayout> {\n\n    @Override\n    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {\n        return OwoUIAdapter.create(this, UIContainers::verticalFlow);\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        rootComponent.surface(Surface.flat(0x77000000))\n            .verticalAlignment(VerticalAlignment.CENTER)\n            .horizontalAlignment(HorizontalAlignment.CENTER);\n\n        var options = IntStream.rangeClosed(1, 25)\n            .mapToObj(value -> new ConfigOption(\"very epic option #\" + value, String.valueOf(value * value)))\n            .toList();\n\n        rootComponent.child(UIComponents.label(\n            Component.literal(\"very epic \").append(Owo.PREFIX).append(\"config\")\n        ).shadow(true).margins(Insets.bottom(15)));\n\n        final var optionsScrollContainer = UIContainers.verticalScroll(\n            Sizing.fill(90),\n            Sizing.fill(85),\n            UIComponents.list(\n                options,\n                flowLayout -> {},\n                this::createOptionComponent,\n                true\n            )\n        );\n\n        rootComponent.child(optionsScrollContainer\n            .scrollbarThiccness(4)\n            .padding(Insets.of(1))\n            .surface(Surface.flat(0x77000000).and(Surface.outline(0xFF121212)))\n        );\n    }\n\n    private FlowLayout createOptionComponent(ConfigOption option) {\n        var container = UIContainers.horizontalFlow(Sizing.fill(100), Sizing.fixed(32));\n        container.padding(Insets.of(5));\n\n        container.child(UIComponents.label(\n            Component.literal(option.name)\n                .withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal(\"text momente\"))))\n        ).positioning(Positioning.relative(0, 50)));\n\n        {\n            var valueLayout = UIContainers.horizontalFlow(Sizing.content(), Sizing.fill(100));\n            valueLayout.positioning(Positioning.relative(100, 50)).verticalAlignment(VerticalAlignment.CENTER);\n            container.child(valueLayout);\n\n            valueLayout.child(UIComponents.slider(Sizing.fixed(200)).message(s -> Component.literal(\"slider for \" + option.name)));\n\n            final var valueBox = UIComponents.textBox(Sizing.fixed(80), option.value);\n            valueLayout.child(valueBox.margins(Insets.horizontal(5)));\n\n            valueLayout.child(UIComponents.button(Component.literal(\"⇄\"), (ButtonComponent button) -> {\n                valueBox.setValue(option.value);\n            }).margins(Insets.right(5)));\n        }\n\n        return container;\n    }\n\n    private record ConfigOption(String name, String value) {}\n\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/TestParseScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.*;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.container.ScrollContainer;\nimport io.wispforest.owo.ui.core.*;\nimport net.minecraft.network.chat.Component;\n\npublic class TestParseScreen extends BaseUIModelScreen<FlowLayout> {\n\n    public TestParseScreen() {\n        super(FlowLayout.class, DataSource.file(\"epic_ui.xml\"));\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n//        rootComponent\n//                .surface(Surface.VANILLA_TRANSLUCENT)\n//                .horizontalAlignment(HorizontalAlignment.CENTER)\n//                .verticalAlignment(VerticalAlignment.CENTER);\n//\n//        rootComponent.child(Components.button(Text.literal(\"A Button\"), button -> {}));\n//\n//        rootComponent.child(Containers.verticalFlow(Sizing.content(), Sizing.content())\n//                .child(Components.button(Text.literal(\"A Button\"), button -> {}))\n//                .padding(Insets.of(10))\n//                .surface(Surface.DARK_PANEL)\n//                .verticalAlignment(VerticalAlignment.CENTER)\n//                .horizontalAlignment(HorizontalAlignment.CENTER)\n//        );\n//\n//        rootComponent.childById(ButtonComponent.class, \"the-button\").onPress(button -> {\n//            System.out.println(\"click\");\n//        });\n\n        var allay = rootComponent.childById(EntityComponent.class, \"allay\");\n        var verticalAnimation = allay.verticalSizing().animate(450, Easing.EXPO, Sizing.fixed(200));\n        var horizontalAnimation = allay.horizontalSizing().animate(450, Easing.CUBIC, Sizing.fixed(200));\n\n        rootComponent.childById(ButtonComponent.class, \"allay-button\").onPress(button -> {\n            verticalAnimation.reverse();\n            horizontalAnimation.reverse();\n            button.setMessage(Component.nullToEmpty(button.getMessage().getString().equals(\"+\") ? \"-\" : \"+\"));\n        });\n\n        rootComponent.childById(TextureComponent.class, \"java-logo\").visibleArea().animate(\n                1000, Easing.SINE, PositionedRectangle.of(0, 0, 128, 16)).forwards();\n\n        var stretchAnimation = rootComponent.childById(ItemComponent.class, \"stretch-item\")\n                .verticalSizing().animate(500, Easing.CUBIC, Sizing.fixed(300));\n        rootComponent.childById(ButtonComponent.class, \"stretch-button\").onPress(button -> stretchAnimation.reverse());\n\n        var flyAnimation = rootComponent.childById(ScrollContainer.class, \"fly\")\n                .positioning().animate(350, Easing.QUADRATIC, Positioning.relative(85, 35));\n        rootComponent.childById(ButtonComponent.class, \"fly-button\").onPress(button -> flyAnimation.reverse());\n\n        var growLabel = rootComponent.childById(LabelComponent.class, \"grow-label\");\n        var growAnimation = growLabel.margins().animate(250, Easing.SINE, Insets.of(15));\n        growLabel.mouseEnter().subscribe(growAnimation::forwards);\n        growLabel.mouseLeave().subscribe(growAnimation::backwards);\n    }\n}"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/TooManyComponentsScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseOwoScreen;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.uwu.items.UwuItems;\nimport net.minecraft.network.chat.Component;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.concurrent.ThreadLocalRandom;\n\npublic class TooManyComponentsScreen extends BaseOwoScreen<FlowLayout> {\n    @Override\n    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {\n        return OwoUIAdapter.create(this, UIContainers::verticalFlow);\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        rootComponent.child(\n                UIContainers.verticalScroll(\n                        Sizing.fill(45), Sizing.fill(45),\n                        UIContainers.verticalFlow(Sizing.content(), Sizing.content()).<FlowLayout>configure(flowLayout -> {\n                            for (int i = 0; i < 50000; i++) {\n                                flowLayout.child(\n                                        UIContainers.collapsible(Sizing.content(), Sizing.content(), Component.nullToEmpty(String.valueOf(ThreadLocalRandom.current().nextInt(100000))), false)\n                                                .child(\n                                                        UIComponents.item(UwuItems.SCREEN_SHARD.getDefaultInstance()).sizing(Sizing.fixed(100))\n                                                )\n                                );\n                            }\n                        })\n                ).surface(Surface.DARK_PANEL).padding(Insets.of(5))\n        ).verticalAlignment(VerticalAlignment.CENTER).horizontalAlignment(HorizontalAlignment.CENTER);\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/UwuClient.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.braid.core.LayoutAxis;\nimport io.wispforest.owo.braid.util.BraidHudElement;\nimport io.wispforest.owo.braid.util.layers.BraidLayersBinding;\nimport io.wispforest.owo.braid.util.BraidTooltipComponent;\nimport io.wispforest.owo.braid.widgets.basic.Box;\nimport io.wispforest.owo.braid.widgets.basic.Clip;\nimport io.wispforest.owo.braid.widgets.basic.Sized;\nimport io.wispforest.owo.braid.widgets.basic.Transform;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.grid.Grid;\nimport io.wispforest.owo.network.OwoNetChannel;\nimport io.wispforest.owo.particles.ClientParticles;\nimport io.wispforest.owo.particles.systems.ParticleSystemController;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.component.EntityComponent;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.owo.ui.hud.Hud;\nimport io.wispforest.owo.ui.layers.Layer;\nimport io.wispforest.owo.ui.layers.Layers;\nimport io.wispforest.owo.ui.parsing.UIModel;\nimport io.wispforest.owo.ui.util.UISounds;\nimport io.wispforest.uwu.Uwu;\nimport io.wispforest.uwu.client.braid.TestSelector;\nimport io.wispforest.uwu.items.UwuBraidItem;\nimport io.wispforest.uwu.network.UwuNetworkExample;\nimport io.wispforest.uwu.network.UwuOptionalNetExample;\nimport net.fabricmc.api.ClientModInitializer;\nimport net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;\nimport net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;\nimport net.fabricmc.fabric.api.client.rendering.v1.TooltipComponentCallback;\nimport net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.PauseScreen;\nimport net.minecraft.client.gui.screens.MenuScreens;\nimport net.minecraft.client.gui.screens.inventory.InventoryScreen;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.KeyMapping;\nimport net.minecraft.client.renderer.blockentity.BlockEntityRenderers;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.entity.animal.allay.Allay;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.core.particles.ParticleTypes;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.contents.TranslatableContents;\nimport net.minecraft.resources.Identifier;\nimport org.joml.Matrix3x2f;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.Random;\nimport java.util.function.Supplier;\nimport java.util.stream.Stream;\n\npublic class UwuClient implements ClientModInitializer {\n\n    @Override\n    public void onInitializeClient() {\n        UwuNetworkExample.Client.init();\n        UwuOptionalNetExample.Client.init();\n\n        MenuScreens.register(Uwu.EPIC_SCREEN_HANDLER_TYPE, EpicContainerScreen::new);\n//        HandledScreens.register(EPIC_SCREEN_HANDLER_TYPE, EpicHandledModelScreen::new);\n\n        final var binding = new KeyMapping(\"key.uwu.hud_test\", GLFW.GLFW_KEY_J, KeyMapping.Category.MISC);\n        KeyBindingHelper.registerKeyBinding(binding);\n\n        final var bindingButCooler = new KeyMapping(\"key.uwu.hud_test_two\", GLFW.GLFW_KEY_K, KeyMapping.Category.MISC);\n        KeyBindingHelper.registerKeyBinding(bindingButCooler);\n\n        final var hudComponentId = Identifier.fromNamespaceAndPath(\"uwu\", \"test_element\");\n        final Supplier<UIComponent> hudComponent = () ->\n            UIContainers.verticalFlow(Sizing.content(), Sizing.content())\n                .child(UIComponents.item(Items.DIAMOND.getDefaultInstance()).margins(Insets.of(3)))\n                .child(UIComponents.label(Component.literal(\"epic stuff in hud\")))\n                .child(UIComponents.entity(Sizing.fixed(50), EntityType.ALLAY, null))\n                .alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER)\n                .padding(Insets.of(5))\n                .surface(Surface.PANEL)\n                .margins(Insets.of(5))\n                .positioning(Positioning.relative(100, 25));\n\n        final var coolerComponentId = Identifier.fromNamespaceAndPath(\"uwu\", \"test_element_two\");\n        final Supplier<UIComponent> coolerComponent = () -> UIModel.load(Path.of(\"../src/testmod/resources/assets/uwu/owo_ui/test_element_two.xml\")).expandTemplate(FlowLayout.class, \"hud-element\", Map.of());\n        Hud.add(coolerComponentId, coolerComponent);\n\n        TooltipComponentCallback.EVENT.register(data -> {\n            if (data instanceof UwuBraidItem.Tooltip tooltip) {\n                var random = new Random(System.currentTimeMillis() / 450);\n                return new BraidTooltipComponent(new Sized(\n                    32 * 5, 32 * 5, new Clip(\n                        true, true,\n                    new Row(\n                        new Transform(\n                            new Matrix3x2f().translation(((float) (System.currentTimeMillis() / 450d - Math.floor(System.currentTimeMillis() / 450d))) * -32, 0),\n                            new Grid(\n                                LayoutAxis.VERTICAL,\n                                6,\n                                Grid.CellFit.loose(),\n                                Stream.generate(() -> new TestSelector.Amogus(\n                                        new Box(io.wispforest.owo.braid.core.Color.hsv(random.nextDouble(), .75, 1)),\n                                        new Box(Color.WHITE.toBraid()),\n                                        8\n                                    ))\n                                    .limit(6 * 5).toList()\n                            )\n                        )\n                    )\n                )\n                ));\n            }\n\n            return null;\n        });\n\n        HudElementRegistry.addLast(\n            Identifier.fromNamespaceAndPath(\"uwu\", \"braid_test\"),\n            new BraidHudElement(new HudTestWidget())\n        );\n\n        ClientTickEvents.END_CLIENT_TICK.register(client -> {\n            while (binding.consumeClick()) {\n                if (Hud.hasComponent(hudComponentId)) {\n                    Hud.remove(hudComponentId);\n                } else {\n                    Hud.add(hudComponentId, hudComponent);\n                }\n            }\n\n            if (bindingButCooler.consumeClick()) {\n                Hud.remove(coolerComponentId);\n                Hud.add(coolerComponentId, coolerComponent);\n\n                //noinspection StatementWithEmptyBody\n                while (bindingButCooler.consumeClick()) {}\n            }\n        });\n\n        Uwu.CHANNEL.registerClientbound(Uwu.OtherTestMessage.class, (message, access) -> {\n            access.player().displayClientMessage(Component.nullToEmpty(\"Message '\" + message.message() + \"' from \" + message.pos()), false);\n        });\n\n        if (Uwu.WE_TESTEN_HANDSHAKE) {\n            OwoNetChannel.create(Identifier.fromNamespaceAndPath(\"uwu\", \"client_only_channel\"));\n\n            Uwu.CHANNEL.registerServerbound(WeirdMessage.class, (data, access) -> {\n            });\n            Uwu.CHANNEL.registerClientbound(WeirdMessage.class, (data, access) -> {\n            });\n\n            new ParticleSystemController(Identifier.fromNamespaceAndPath(\"uwu\", \"client_only_particles\"));\n            Uwu.PARTICLE_CONTROLLER.register(WeirdMessage.class, (world, pos, data) -> {\n            });\n        }\n\n        Uwu.CUBE.setHandler((world, pos, data) -> {\n            ClientParticles.setParticleCount(5);\n            ClientParticles.spawnCubeOutline(ParticleTypes.END_ROD, world, pos, 1, .01f);\n        });\n\n        Layers.add(UIContainers::verticalFlow, instance -> {\n            if (Minecraft.getInstance().level == null) return;\n\n            instance.adapter.rootComponent.child(\n                UIContainers.horizontalFlow(Sizing.content(), Sizing.content())\n                    .child(UIComponents.entity(Sizing.fixed(20), EntityType.ALLAY, null).<EntityComponent<Allay>>configure(component -> {\n                        component.allowMouseRotation(true)\n                            .scale(.75f);\n\n                        component.mouseDown().subscribe((click, doubled) -> {\n                            UISounds.playInteractionSound();\n                            return true;\n                        });\n                    })).child(UIComponents.textBox(Sizing.fixed(100), \"allay text\").<EditBox>configure(textBox -> {\n                        textBox.verticalSizing(Sizing.fixed(9));\n                        textBox.setBordered(false);\n                    })).<FlowLayout>configure(layout -> {\n                        layout.gap(5).margins(Insets.left(4)).verticalAlignment(VerticalAlignment.CENTER);\n\n                        instance.alignComponentToWidget(widget -> {\n                            if (!(widget instanceof Button button)) return false;\n                            return button.getMessage().getContents() instanceof TranslatableContents translatable && translatable.getKey().equals(\"gui.stats\");\n                        }, Layer.Instance.AnchorSide.RIGHT, 0, layout);\n                    })\n            );\n        }, PauseScreen.class);\n\n        Layers.add(UIContainers::verticalFlow, instance -> {\n            ButtonComponent button;\n            instance.adapter.rootComponent.child(\n                (button = UIComponents.button(Component.literal(\":)\"), buttonComponent -> {\n                    Minecraft.getInstance().player.displayClientMessage(Component.literal(\"handled screen moment\"), false);\n                })).verticalSizing(Sizing.fixed(12))\n            );\n\n            instance.alignComponentToHandledScreenCoordinates(button, 125, 65);\n        }, InventoryScreen.class);\n\n        BraidLayersBinding.add(\n            screen -> screen instanceof InventoryScreen,\n            new LayersTestWidget()\n        );\n\n        BlockEntityRenderers.register(Uwu.BRAID_DISPLAY_ENTITY, BraidDisplayBlockEntityRenderer::new);\n    }\n\n    public record WeirdMessage(int e) {}\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/UwuConfigScreen.java",
    "content": "package io.wispforest.uwu.client;\n\nimport io.wispforest.owo.ui.base.BaseUIModelScreen;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.SliderComponent;\nimport io.wispforest.owo.ui.container.FlowLayout;\nimport io.wispforest.owo.ui.core.UIComponent;\nimport net.minecraft.client.gui.components.EditBox;\n\nimport java.util.Map;\n\npublic class UwuConfigScreen extends BaseUIModelScreen<FlowLayout> {\n\n    public UwuConfigScreen() {\n        super(FlowLayout.class, DataSource.file(\"config.xml\"));\n    }\n\n    @Override\n    protected void build(FlowLayout rootComponent) {\n        var panel = rootComponent.childById(FlowLayout.class, \"config-panel\");\n\n        for (int i = 1; i <= 25; i++) {\n            panel.child(i % 2 == 0\n                    ? this.createTextOption(i)\n                    : this.createRangeOption(i)\n            );\n        }\n    }\n\n    protected UIComponent createTextOption(final int index) {\n        var option = this.model.expandTemplate(FlowLayout.class,\n                \"text-config-option\",\n                Map.of(\n                        \"config-option-name\", \"very epic option #\" + index,\n                        \"config-option-value\", String.valueOf(index * index)\n                )\n        );\n\n        var valueBox = option.childById(EditBox.class, \"value-box\");\n        option.childById(ButtonComponent.class, \"reset-button\").onPress(button -> {\n            valueBox.setValue(String.valueOf(index * index));\n        });\n\n        return option;\n    }\n\n    protected UIComponent createRangeOption(final int index) {\n        var option = this.model.expandTemplate(FlowLayout.class,\n                \"range-config-option\",\n                Map.of(\n                        \"config-option-name\", \"very epic option #\" + index,\n                        \"config-option-value\", String.valueOf(index * index)\n                )\n        );\n\n        var valueSlider = option.childById(SliderComponent.class, \"value-slider\");\n        valueSlider.value((index * index) / 625d);\n\n        option.childById(ButtonComponent.class, \"reset-button\").onPress(button -> {\n            valueSlider.value((index * index) / 625d);\n        });\n\n        return option;\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/braid/SliderTests.java",
    "content": "package io.wispforest.uwu.client.braid;\n\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.framework.widget.WidgetSetupCallback;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.MessageButton;\nimport io.wispforest.owo.braid.widgets.flex.Column;\nimport io.wispforest.owo.braid.widgets.flex.CrossAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.MainAxisAlignment;\nimport io.wispforest.owo.braid.widgets.flex.Row;\nimport io.wispforest.owo.braid.widgets.grid.Grid;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.scroll.VerticallyScrollable;\nimport io.wispforest.owo.braid.widgets.slider.SliderStyle;\nimport io.wispforest.owo.braid.widgets.slider.drag.MessageDrag;\nimport io.wispforest.owo.braid.widgets.slider.range.DefaultRangeSliderStyle;\nimport io.wispforest.owo.braid.widgets.slider.range.MessageRangeSlider;\nimport io.wispforest.owo.braid.widgets.slider.range.RangeSliderStyle;\nimport io.wispforest.owo.braid.widgets.slider.slider.MessageSlider;\nimport io.wispforest.owo.braid.widgets.slider.slider.Slider;\nimport io.wispforest.owo.braid.widgets.slider.slider.SliderFunction;\nimport io.wispforest.owo.braid.widgets.slider.xlyder.DefaultXlyderStyle;\nimport io.wispforest.owo.braid.widgets.slider.xlyder.MessageXlyder;\nimport io.wispforest.owo.braid.widgets.slider.xlyder.Xlyder;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.ui.core.OwoUIGraphics;\nimport io.wispforest.uwu.client.Bikeshed;\nimport net.minecraft.sounds.SoundEvents;\nimport net.minecraft.network.chat.Component;\nimport org.joml.Matrix3x2f;\n\nimport java.math.BigDecimal;\nimport java.math.RoundingMode;\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class SliderTests extends StatefulWidget {\n\n    public enum SliderTest {\n        BASIC, DIRECTION, REDUNDANT, SLIDER\n    }\n\n    @Override\n    public WidgetState<SliderTests> createState() {\n        return new State();\n    }\n\n    public static class State extends WidgetState<SliderTests> {\n\n        private SliderTest test = SliderTest.BASIC;\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Stack(\n                Alignment.CENTER,\n                new DefaultXlyderStyle(\n                    new SliderStyle<>(\n                        null,\n                        active -> new HoverableBuilder((hoverableContext, hovered) -> {\n                            return hovered ? new TestSelector.GayAmogus(4) : new Box(Color.WHITE);\n                        }),\n                        Size.of(16, 16),\n                        null\n                    ),\n                    new DefaultRangeSliderStyle(\n                        new RangeSliderStyle(null, new Bikeshed(), null, null, null, null, Optional.of(SoundEvents.ANVIL_LAND)),\n                        switch (this.test) {\n                            case BASIC -> new BasicSliderTest();\n                            case DIRECTION -> new SliderDirectionTest();\n                            case REDUNDANT -> new IncrediblyRedundantSlider();\n                            case SLIDER -> new NormalSliderTest();\n                        }\n                    )\n                ),\n                new Align(\n                    Alignment.TOP_RIGHT,\n                    new HitTestTrap(\n                        new Padding(\n                            Insets.all(5),\n                            new Panel(\n                                OwoUIGraphics.PANEL_NINE_PATCH_TEXTURE,\n                                new Padding(\n                                    Insets.all(5),\n                                    new Sized(\n                                        null, 66,\n                                        new VerticallyScrollable(\n                                            new IntrinsicWidth(\n                                                new Column(\n                                                    MainAxisAlignment.START,\n                                                    CrossAxisAlignment.CENTER,\n                                                    new Padding(Insets.all(2)),\n                                                    Stream.of(SliderTest.values()).map(test -> new MessageButton(\n                                                        Component.literal(test.name().toLowerCase(Locale.ROOT).replace('_', ' ')),\n                                                        test != this.test ? () -> this.setState(() -> this.test = test) : null\n                                                    )).collect(Collectors.toList())\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    public static String formatDouble(double value) {\n        return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).toPlainString().replaceAll(\"(\\\\.0*|(?<=\\\\d)\\\\.0+)$\", \"\");\n    }\n\n\n    public static class BasicSliderTest extends StatefulWidget {\n\n        @Override\n        public WidgetState<BasicSliderTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<BasicSliderTest> {\n            private double discreteX = 16;\n            private double discreteY = 16;\n            private double smoothX = 16;\n            private double smoothY = 16;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Grid(\n                    LayoutAxis.VERTICAL,\n                    3,\n                    Grid.CellFit.tight(),\n                    widget -> new Padding(Insets.all(5), widget),\n                    null,\n                    Label.literal(\"Discrete\"),\n                    Label.literal(\"Smooth\"),\n                    Label.literal(\"Basic\"),\n                    new Sized(\n                        100, 20,\n                        new MessageSlider(\n                            discreteX,\n                            Component.literal(\"v: \" + formatDouble(discreteX)), widget -> widget\n                                .range(0, 32)\n                                .step(2),\n                            newValue -> this.setState(() -> this.discreteX = newValue)\n                        )\n                    ),\n                    new Sized(\n                        100, 20,\n                        new MessageSlider(\n                            smoothX,\n                            Component.literal(\"v: \" + formatDouble(smoothX)), widget -> widget.range(0, 32),\n                            newValue -> this.setState(() -> this.smoothX = newValue)\n                        )\n                    ),\n                    Label.literal(\"XY\"),\n                    new Sized(\n                        100, 100,\n                        new MessageXlyder(\n                            discreteX, discreteY,\n                            Component.literal(\"x: \" + formatDouble(discreteX) + \"\\ny: \" + formatDouble(discreteY)),\n                            xlyder -> xlyder\n                                .range(0, 32)\n                                .step(2),\n                            (x, y) -> this.setState(() -> {\n                                this.discreteX = x;\n                                this.discreteY = y;\n                            })\n                        )\n                    ),\n                    new Sized(\n                        100, 100,\n                        new MessageXlyder(\n                            smoothX, smoothY,\n                            Component.literal(\"x: \" + formatDouble(smoothX) + \"\\ny: \" + formatDouble(smoothY)),\n                            xlyder -> xlyder.range(0, 32),\n                            (x, y) -> this.setState(() -> {\n                                this.smoothX = x;\n                                this.smoothY = y;\n                            })\n                        )\n                    ),\n                    Label.literal(\"Range\"),\n                    new Sized(\n                        100, 20,\n                        new MessageRangeSlider(\n                            discreteX, discreteY,\n                            Component.literal(\"v: \" + formatDouble(discreteX) + \"-\" + formatDouble(discreteY)), slider -> slider\n                                .range(0, 32)\n                                .step(2),\n                            (min, max) -> this.setState(() -> {\n                                this.discreteX = min;\n                                this.discreteY = max;\n                            })\n                        )\n                    ),\n                    new Sized(\n                        100, 20,\n                        new MessageRangeSlider(\n                            smoothX, smoothY,\n                            Component.literal(\"v: \" + formatDouble(smoothX) + \"-\" + formatDouble(smoothY)), slider -> slider.range(0, 32),\n                            (min, max) -> this.setState(() -> {\n                                this.smoothX = min;\n                                this.smoothY = max;\n                            })\n                        )\n                    ),\n                    Label.literal(\"Drag\"),\n                    new Sized(\n                        100, 20,\n                        new MessageDrag(\n                            discreteX,\n                            drag -> drag.range(0, 32).step(2),\n                            newValue -> this.setState(() -> this.discreteX = newValue),\n                            Component.literal(\"v: \" + formatDouble(discreteX))\n                        )\n                    ),\n                    new Sized(\n                        100, 20,\n                        new MessageDrag(\n                            smoothX,\n                            drag -> drag.range(0, 32),\n                            newValue -> this.setState(() -> this.smoothX = newValue),\n                            Component.literal(\"v: \" + formatDouble(smoothX))\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class NormalSliderTest extends StatefulWidget {\n\n        @Override\n        public WidgetState<NormalSliderTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<NormalSliderTest> {\n            private double value = 16;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Grid(\n                    LayoutAxis.VERTICAL,\n                    3,\n                    Grid.CellFit.tight(),\n                    widget -> new Padding(Insets.all(5), widget),\n                    null,\n                    Label.literal(\"Discrete\"),\n                    Label.literal(\"Smooth\"),\n                    Label.literal(\"Linear\"),\n                    slider(widget -> widget.range(0, 32).step(2)),\n                    slider(widget -> widget.range(0, 32)),\n                    Label.literal(\"Logarithmic\"),\n                    slider(widget -> widget.range(0, 32).step(2).function(SliderFunction.LOGARITHMIC)),\n                    slider(widget -> widget.range(0, 32).function(SliderFunction.LOGARITHMIC)),\n                    Label.literal(\"Reverse\"),\n                    slider(widget -> widget.range(32, 0).step(2)),\n                    slider(widget -> widget.range(32, 0)),\n                    Label.literal(\"Logarithmic Reverse\"),\n                    slider(widget -> widget.range(32, 0).step(2).function(SliderFunction.LOGARITHMIC)),\n                    slider(widget -> widget.range(32, 0).function(SliderFunction.LOGARITHMIC))\n                );\n            }\n\n            private Widget slider(WidgetSetupCallback<Slider> setupCallback) {\n                return new Sized(\n                    100, 20,\n                    new MessageSlider(\n                        value,\n                        Component.literal(\"v: \" + formatDouble(value)), setupCallback,\n                        newValue -> this.setState(() -> this.value = newValue)\n                    )\n                );\n            }\n        }\n    }\n\n\n    public static class SliderDirectionTest extends StatefulWidget {\n        @Override\n        public WidgetState<SliderDirectionTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<SliderDirectionTest> {\n\n            private static final double min = 0, max = 32;\n\n            private double x = 16;\n            private double y = 16;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Grid(\n                    LayoutAxis.VERTICAL,\n                    3,\n                    Grid.CellFit.tight(),\n                    new Sized(\n                        100, 100,\n                        new Xlyder(\n                            x, y,\n                            xlyder -> xlyder.rangeX(max, min).rangeY(min, max).incrementStep(1), (x, y) -> this.setState(() -> {\n                            this.x = x;\n                            this.y = y;\n                        })\n                        )\n                    ),\n                    new Sized(\n                        20, 100,\n                        new Slider(\n                            y,\n                            slider -> slider.range(min, max).vertical().incrementStep(1), newValue -> this.setState(() -> this.y = newValue)\n                        )\n                    ),\n                    new Sized(\n                        100, 100,\n                        new Xlyder(\n                            x, y,\n                            xlyder -> xlyder.range(min, max).incrementStep(1), (x, y) -> this.setState(() -> {\n                            this.x = x;\n                            this.y = y;\n                        })\n                        )\n                    ),\n                    new Sized(\n                        100, 20,\n                        new Slider(\n                            x,\n                            slider -> slider.range(max, min).incrementStep(1), newValue -> this.setState(() -> this.x = newValue)\n                        )\n                    ),\n                    new Sized(\n                        20, 20,\n                        new Transform(\n                            new Matrix3x2f().scale(0.5f),\n                            new Label(\n                                null,\n                                false,\n                                Label.Overflow.SHOW,\n                                Component.literal(formatDouble(x) + \"\\n\" + formatDouble(y))\n                            )\n                        )\n                    ),\n                    new Sized(\n                        100, 20,\n                        new Slider(\n                            x,\n                            slider -> slider.range(min, max).incrementStep(1), newValue -> this.setState(() -> this.x = newValue)\n                        )\n                    ),\n                    new Sized(\n                        100, 100,\n                        new Xlyder(\n                            x, y,\n                            xlyder -> xlyder.range(max, min).incrementStep(1), (x, y) -> this.setState(() -> {\n                            this.x = x;\n                            this.y = y;\n                        })\n                        )\n                    ),\n                    new Sized(\n                        20, 100,\n                        new Slider(\n                            y,\n                            slider -> slider.range(max, min).vertical().incrementStep(1), newValue -> this.setState(() -> this.y = newValue)\n                        )\n                    ),\n                    new Sized(\n                        100, 100,\n                        new Xlyder(\n                            x, y,\n                            xlyder -> xlyder.rangeX(min, max).rangeY(max, min).incrementStep(1), (x, y) -> this.setState(() -> {\n                            this.x = x;\n                            this.y = y;\n                        })\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class IncrediblyRedundantSlider extends StatefulWidget {\n        @Override\n        public WidgetState<IncrediblyRedundantSlider> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<IncrediblyRedundantSlider> {\n\n            private double x, y;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Column(\n                    MainAxisAlignment.START,\n                    CrossAxisAlignment.CENTER,\n                    new Padding(\n                        Insets.all(20),\n                        new Label(Component.literal(\"incredibly redundant slider™\"))\n                    ),\n                    new Sized(\n                        100.0,\n                        15.0,\n                        new Slider(\n                            this.x,\n                            null, (x) -> this.setState(() -> this.x = x)\n                        )\n                    ),\n                    new Row(\n                        new Sized(\n                            15.0,\n                            100.0,\n                            new Slider(\n                                this.y,\n                                Slider::vertical, (y) -> this.setState(() -> this.y = y)\n                            )\n                        ),\n                        new Sized(\n                            100.0,\n                            100.0,\n                            new Xlyder(\n                                this.x, this.y,\n                                xlyder -> xlyder.style(new SliderStyle<>(null, null, Size.square((1 - this.y) * 16 + 8), null)), (x, y) -> this.setState(() -> {\n                                this.x = x;\n                                this.y = y;\n                            })\n                            )\n                        ),\n                        new Sized(\n                            15.0,\n                            100.0,\n                            new Slider(\n                                this.y,\n                                Slider::vertical, y -> this.setState(() -> this.y = y)\n                            )\n                        )\n                    ),\n                    new Sized(\n                        100.0,\n                        15.0,\n                        new Slider(\n                            this.x,\n                            slider -> slider.style(new SliderStyle<>(null, null, 24.0, null)), x -> this.setState(() -> this.x = x)\n                        )\n                    ),\n                    new Sized(\n                        100.0,\n                        15.0,\n                        new Slider(\n                            this.x,\n                            slider -> slider.style(new SliderStyle<>(null, null, 18.0, Optional.empty())), x -> this.setState(() -> this.x = x)\n                        )\n                    ),\n                    new Sized(\n                        100.0,\n                        15.0,\n                        new Slider(\n                            this.x,\n                            slider -> slider.style(new SliderStyle<>(null, null, 12.0, null)), x -> this.setState(() -> this.x = x)\n                        )\n                    ),\n                    new Sized(\n                        100.0,\n                        15.0,\n                        new Slider(\n                            this.x,\n                            slider -> slider.style(new SliderStyle<>(null, null, 6.0, null)), (x) -> this.setState(() -> this.x = x)\n                        )\n                    )\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/client/braid/TestSelector.java",
    "content": "package io.wispforest.uwu.client.braid;\n\nimport com.google.gson.GsonBuilder;\nimport com.mojang.authlib.GameProfile;\nimport com.mojang.blaze3d.platform.InputConstants;\nimport com.mojang.math.Axis;\nimport dev.kdl.KdlNode;\nimport dev.kdl.parse.Kdl2Parser;\nimport io.wispforest.endec.SerializationAttributes;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.endec.format.gson.GsonSerializer;\nimport io.wispforest.owo.braid.animation.*;\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.core.cursor.CursorStyle;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.util.BraidToast;\nimport io.wispforest.owo.braid.util.kdl.BraidKdlEndecs;\nimport io.wispforest.owo.braid.util.kdl.KdlDeserializer;\nimport io.wispforest.owo.braid.util.kdl.KdlMapper;\nimport io.wispforest.owo.braid.util.kdl.WidgetEndec;\nimport io.wispforest.owo.braid.widgets.*;\nimport io.wispforest.owo.braid.widgets.animated.AnimatedAlign;\nimport io.wispforest.owo.braid.widgets.animated.AnimatedBox;\nimport io.wispforest.owo.braid.widgets.animated.AnimatedPadding;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.*;\nimport io.wispforest.owo.braid.widgets.checkbox.Checkbox;\nimport io.wispforest.owo.braid.widgets.checkbox.CheckboxStyle;\nimport io.wispforest.owo.braid.widgets.checkbox.DefaultCheckboxStyle;\nimport io.wispforest.owo.braid.widgets.combobox.ComboBox;\nimport io.wispforest.owo.braid.widgets.cycle.MessageCyclingButton;\nimport io.wispforest.owo.braid.widgets.drag.DragArena;\nimport io.wispforest.owo.braid.widgets.drag.DragArenaElement;\nimport io.wispforest.owo.braid.widgets.flex.*;\nimport io.wispforest.owo.braid.widgets.focus.FocusLevel;\nimport io.wispforest.owo.braid.widgets.focus.FocusPolicy;\nimport io.wispforest.owo.braid.widgets.focus.Focusable;\nimport io.wispforest.owo.braid.widgets.grid.Grid;\nimport io.wispforest.owo.braid.widgets.intents.*;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.object.BlockWidget;\nimport io.wispforest.owo.braid.widgets.object.EntityWidget;\nimport io.wispforest.owo.braid.widgets.object.ItemStackWidget;\nimport io.wispforest.owo.braid.widgets.overlay.Overlay;\nimport io.wispforest.owo.braid.widgets.overlay.OverlayEntryBuilder;\nimport io.wispforest.owo.braid.widgets.owoui.OwoUIWidget;\nimport io.wispforest.owo.braid.widgets.recipeviewer.RecipeViewerExclusionZone;\nimport io.wispforest.owo.braid.widgets.recipeviewer.RecipeViewerStack;\nimport io.wispforest.owo.braid.widgets.recipeviewer.StackDropArea;\nimport io.wispforest.owo.braid.widgets.scroll.*;\nimport io.wispforest.owo.braid.widgets.sharedstate.ShareableState;\nimport io.wispforest.owo.braid.widgets.sharedstate.SharedState;\nimport io.wispforest.owo.braid.widgets.slider.Incrementor;\nimport io.wispforest.owo.braid.widgets.slider.slider.MessageSlider;\nimport io.wispforest.owo.braid.widgets.slider.slider.Slider;\nimport io.wispforest.owo.braid.widgets.slider.xlyder.MessageXlyder;\nimport io.wispforest.owo.braid.widgets.splitpane.MultiSplitPane;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport io.wispforest.owo.braid.widgets.textinput.MaxLengthFormatter;\nimport io.wispforest.owo.braid.widgets.textinput.PatternFormatter;\nimport io.wispforest.owo.braid.widgets.textinput.TextBox;\nimport io.wispforest.owo.braid.widgets.textinput.TextEditingController;\nimport io.wispforest.owo.braid.widgets.vanilla.VanillaWidget;\nimport io.wispforest.owo.braid.widgets.window.Window;\nimport io.wispforest.owo.braid.widgets.window.WindowController;\nimport io.wispforest.owo.ops.TextOps;\nimport io.wispforest.owo.ui.component.BraidComponent;\nimport io.wispforest.owo.ui.component.EntityComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.core.Sizing;\nimport io.wispforest.owo.ui.util.Delta;\nimport io.wispforest.owo.util.EventSource;\nimport io.wispforest.owo.util.ViewerStack;\nimport io.wispforest.owo.util.Wisdom;\nimport io.wispforest.uwu.client.Bikeshed;\nimport io.wispforest.uwu.client.HudTestWidget;\nimport io.wispforest.uwu.items.UwuItems;\nimport net.minecraft.ChatFormatting;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.resources.model.Material;\nimport net.minecraft.client.resources.sounds.SimpleSoundInstance;\nimport net.minecraft.core.BlockPos;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.network.chat.*;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.sounds.SoundEvents;\nimport net.minecraft.tags.ItemTags;\nimport net.minecraft.util.CommonColors;\nimport net.minecraft.util.Mth;\nimport net.minecraft.util.RandomSource;\nimport net.minecraft.util.Util;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.EntitySpawnReason;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.entity.LivingEntity;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.BlockItem;\nimport net.minecraft.world.item.ItemDisplayContext;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.Items;\nimport org.jetbrains.annotations.Nullable;\nimport org.joml.Matrix3x2f;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.math.BigInteger;\nimport java.net.URI;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.function.IntConsumer;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static io.wispforest.uwu.client.braid.SliderTests.formatDouble;\n\npublic class TestSelector extends StatefulWidget {\n\n    public enum Tests {\n        COUNTER,\n        FLEX,\n        DRAGGING,\n        SPLIT_PANE,\n        SLIDERS,\n        TEXT_INPUT,\n        BURNING_CHYZ,\n        SCROLLING,\n        INPUT,\n        CYCLING,\n        VANILLA,\n        SHARED_STATE,\n        STACKS,\n        GRIDS,\n        CONTRIBUTORS,\n        ANIMATIONS,\n        NAVIGATOR,\n        OVERLAY,\n        TEXT,\n        SPINNY_GHAST,\n        OPTIMIZATION,\n        AUTOMATIC_ANIMATION,\n        KDL_WIDGETS\n    }\n\n    @Override\n    public WidgetState<TestSelector> createState() {\n        return new State();\n    }\n\n    public static class BurningChyz extends ShareableState {\n        public final Player chyz;\n        public BurningChyz(Player chyz) {this.chyz = chyz;}\n\n        public static Player of(BuildContext context) {\n            return SharedState.getWithoutDependency(context, BurningChyz.class).chyz;\n        }\n    }\n\n    public static class State extends WidgetState<TestSelector> {\n\n        private double xSkew = 0f;\n        private double ySkew = 0f;\n\n        private double rotat = 0f;\n        private int fliptat = 0;\n        private boolean bouncy = false;\n\n        private Tests test = null;\n        private Player chyz;\n\n        @Override\n        public void init() {\n            this.chyz = EntityComponent.createRenderablePlayer(new GameProfile(\n                UUID.fromString(\"09de8a6d-86bf-4c15-bb93-ce3384ce4e96\"),\n                \"chyzman\"\n            ));\n\n            this.chyz.setSharedFlagOnFire(true);\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            var buttons = Arrays.stream(Tests.values()).map(test -> {\n                if (test == Tests.BURNING_CHYZ) {\n                    return new BurningChyzButton(this.chyz, () -> setState(() -> this.test = Tests.BURNING_CHYZ));\n                } else {\n                    return (Widget) new MessageButton(\n                        Component.literal(test.name().toLowerCase(Locale.ROOT).replace('_', ' ')),\n                        this.test != test ? () -> setState(() -> this.test = test) : null\n                    );\n                }\n            }).collect(Collectors.toList());\n\n            buttons.add(\n                new MessageButton(\n                    Component.literal(\"window\"),\n                    () -> BraidWindow.open(\n                        \"window moment??\",\n                        1200,\n                        800,\n                        new Box(\n                            Color.rgb(0x1d2026),\n                            new TestSelector()\n                        )\n                    )\n                )\n            );\n\n            return new DefaultCheckboxStyle(\n                new CheckboxStyle(null, null, SoundEvents.ENDER_DRAGON_FLAP),\n                new Stack(\n                    Alignment.CENTER,\n                    new RotatedLayout(\n                        this.fliptat,\n                        new Stack(\n                            Alignment.CENTER,\n                            new Transform(\n                                Util.make(() -> {\n                                    var mat = new Matrix3x2f();\n                                    mat.m01 = (float) Math.tan(this.xSkew);\n                                    mat.m10 = (float) Math.tan(this.ySkew);\n                                    mat.rotate((float) Math.toRadians(this.rotat));\n                                    return mat;\n                                }),\n                                new SharedState<>(\n                                    () -> new BurningChyz(this.chyz),\n                                    new Center(\n                                        switch (this.test) {\n                                            case COUNTER -> new Counter();\n                                            case FLEX -> new FunnySwitchLayout();\n                                            case DRAGGING -> new DragArenaTest();\n                                            case SPLIT_PANE -> new SplitPaneTest();\n                                            case SLIDERS -> new SliderTests();\n                                            case TEXT_INPUT -> new TextInputTest();\n                                            case BURNING_CHYZ -> new BurningChyzTest();\n                                            case SCROLLING -> new ScrollTest();\n                                            case INPUT -> new InputTest();\n                                            case CYCLING -> new CyclingTest();\n                                            case VANILLA -> new VanillaTest();\n                                            case SHARED_STATE -> new SharedStateTest();\n                                            case STACKS -> new StacksTest();\n                                            case GRIDS -> new GridsTest();\n                                            case CONTRIBUTORS -> new ContributorsTest();\n                                            case ANIMATIONS -> new AnimationsTest();\n                                            case NAVIGATOR -> new NavigatorTest();\n                                            case OVERLAY -> new OverlayTest();\n                                            case TEXT -> new TextTest();\n                                            case SPINNY_GHAST -> new SpinnyGhastTest();\n                                            case OPTIMIZATION -> new OptimizationTest();\n                                            case AUTOMATIC_ANIMATION -> new AutomaticAnimationTest();\n                                            case KDL_WIDGETS -> new KdlWidgetsTest();\n                                            case null -> new Center(new Label(Component.literal(\"select a test\")));\n                                        }\n                                    )\n                                )\n                            ),\n                            new Align(\n                                Alignment.LEFT,\n                                new Padding(\n                                    Insets.vertical(50).withLeft(5),\n                                    new HitTestTrap(\n                                        new Panel(\n                                            Panel.VANILLA_LIGHT,\n                                            new Padding(\n                                                Insets.all(8),\n                                                new IntrinsicWidth(\n                                                    new Column(\n                                                        new Flexible(\n                                                            new Panel(\n                                                                Panel.VANILLA_INSET,\n                                                                new Padding(\n                                                                    Insets.all(2),\n                                                                    new VerticallyScrollable(\n                                                                        null,\n                                                                        this.bouncy\n                                                                            ? new ScrollAnimationSettings(Duration.ofMillis(750), Easing.OUT_BOUNCE)\n                                                                            : new ScrollAnimationSettings(Duration.ofMillis(250), Easing.OUT_EXPO),\n\n                                                                        new Column(\n                                                                            new Padding(Insets.all(2)),\n                                                                            buttons\n                                                                        )\n                                                                    )\n                                                                )\n                                                            )\n                                                        ),\n                                                        new Padding(Insets.vertical(3)),\n                                                        new Row(\n                                                            MainAxisAlignment.SPACE_AROUND,\n                                                            CrossAxisAlignment.CENTER,\n                                                            new Checkbox(CheckboxStyle.BRAID, this.bouncy, nowChecked -> this.setState(() -> this.bouncy = nowChecked)),\n                                                            new Label(\n                                                                LabelStyle.SHADOW,\n                                                                true, Component.literal(\"bouncy?\")\n                                                            )\n                                                        )\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    ),\n                    new Align(\n                        Alignment.BOTTOM_RIGHT,\n                        new Row(\n                            MainAxisAlignment.START,\n                            CrossAxisAlignment.END,\n                            new Padding(\n                                Insets.all(5),\n                                new SurfaceDimensions()\n                            ),\n                            new Sized(\n                                75, null,\n                                new Column(\n                                    new Sized(\n                                        75, 20,\n                                        new FocusPolicy(\n                                            false,\n                                            new Grid(\n                                                LayoutAxis.VERTICAL,\n                                                4,\n                                                Grid.CellFit.tight(),\n                                                new MessageButton(\n                                                    Component.literal(\"↑\"),\n                                                    () -> Actions.invoke(Focusable.of(context).primaryFocus().context(), new Incrementor.IncrementIntent(LayoutAxis.VERTICAL, 1))\n                                                ),\n                                                new MessageButton(\n                                                    Component.literal(\"↓\"),\n                                                    () -> Actions.invoke(Focusable.of(context).primaryFocus().context(), new Incrementor.IncrementIntent(LayoutAxis.VERTICAL, -1))\n                                                ),\n                                                new MessageButton(\n                                                    Component.literal(\"←\"),\n                                                    () -> Actions.invoke(Focusable.of(context).primaryFocus().context(), new Incrementor.IncrementIntent(LayoutAxis.HORIZONTAL, -1))\n                                                ),\n                                                new MessageButton(\n                                                    Component.literal(\"→\"),\n                                                    () -> Actions.invoke(Focusable.of(context).primaryFocus().context(), new Incrementor.IncrementIntent(LayoutAxis.HORIZONTAL, 1))\n                                                )\n                                            )\n                                        )\n                                    ),\n                                    new Sized(\n                                        75, 20,\n                                        new ListenableBuilder(\n                                            HudTestWidget.SHOW_TEST_HUD,\n                                            listenableContext -> MessageCyclingButton.forBoolean(\n                                                HudTestWidget.SHOW_TEST_HUD.value(),\n                                                Component.literal(\"hud: \" + (HudTestWidget.SHOW_TEST_HUD.value() ? \"on\" : \"off\")),\n                                                (newValue, newIndex) -> HudTestWidget.SHOW_TEST_HUD.setValue(newValue)\n                                            )\n                                        )\n                                    ),\n                                    new Sized(\n                                        75, 20,\n                                        new MessageButton(\n                                            Component.literal(\"yum\"),\n                                            () -> BraidToast.show(\n                                                Duration.ofSeconds(5),\n                                                null,\n                                                new Row(\n                                                    Stream.generate(() -> new Amogus(\n                                                        new Box(Color.randomHue()),\n                                                        new Box(Color.WHITE),\n                                                        8\n                                                    )).limit(100).toList()\n                                                )\n                                            )\n                                        )\n                                    ),\n                                    new Sized(\n                                        75, 20,\n                                        new MessageButton(\n                                            Component.literal(\"reset\"),\n                                            () -> this.setState(() -> {\n                                                this.xSkew = 0f;\n                                                this.ySkew = 0f;\n                                                this.rotat = 0f;\n                                                this.fliptat = 0;\n                                            })\n                                        )\n                                    ),\n                                    new Sized(\n                                        75,\n                                        null,\n                                        new Column(\n                                            new Padding(\n                                                Insets.top(5),\n                                                new Label(Component.literal(\"fliptat:\"))\n                                            ),\n                                            new Row(\n                                                MainAxisAlignment.START,\n                                                CrossAxisAlignment.CENTER,\n                                                new Flexible(\n                                                    new MessageButton(Component.literal(\"-\"), () -> this.setState(() -> this.fliptat -= 1))\n                                                ),\n                                                new Flexible(\n                                                    new Label(Component.literal(String.valueOf(this.fliptat)))\n                                                ),\n                                                new Flexible(\n                                                    new MessageButton(Component.literal(\"+\"), () -> this.setState(() -> this.fliptat += 1))\n                                                )\n                                            )\n                                        )\n                                    ),\n                                    new Sized(\n                                        75, 20,\n                                        new MessageSlider(\n                                            rotat,\n                                            Component.literal(\"rotat: \" + formatDouble(this.rotat)), widget -> widget.range(0, 360).incrementStep(1),\n                                            value -> this.setState(() -> this.rotat = value)\n                                        )\n                                    ),\n                                    new Sized(\n                                        75.0,\n                                        75.0,\n                                        new MessageXlyder(\n                                            this.xSkew, this.ySkew,\n                                            Component.literal(\"x skew: \" + (formatDouble(this.xSkew)) + \"\\ny skew: \" + (formatDouble(this.ySkew))), xlyder -> xlyder.range(-.75, .75),\n\n                                            (xValue, yValue) -> this.setState(() -> {\n                                                this.xSkew = xValue;\n                                                this.ySkew = yValue;\n                                            })\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    public static class SurfaceDimensions extends StatefulWidget {\n        @Override\n        public WidgetState<SurfaceDimensions> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<SurfaceDimensions> {\n\n            private EventSource<Surface.ResizeCallback>.Subscription listener;\n\n            private int width, height;\n\n            @Override\n            public void init() {\n                var surface = AppState.of(this.context()).surface;\n                this.width = surface.width();\n                this.height = surface.height();\n\n                this.listener = surface.onResize().subscribe((newWidth, newHeight) -> this.setState(() -> {\n                    this.width = newWidth;\n                    this.height = newHeight;\n                }));\n            }\n\n            @Override\n            public void dispose() {\n                this.listener.cancel();\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Label(\n                    LabelStyle.SHADOW,\n                    true,\n                    Component.literal(\n                        \"surface dimensions:\\n\" + this.width + \", \" + this.height\n                    ).withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal(\"this is hover text\"))))\n                );\n            }\n        }\n    }\n\n    public static class Counter extends StatefulWidget {\n        @Override\n        public WidgetState<Counter> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<Counter> {\n            private int count = 0;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Sized(\n                    50.0,\n                    null,\n                    new Column(\n                        new Label(Component.literal(\"count: \" + this.count)),\n                        new Row(\n                            new Flexible(\n                                new MessageButton(\n                                    Component.literal(\"+\"),\n                                    () -> this.setState(() -> this.count++)\n                                )\n                            ),\n                            new Flexible(\n                                new MessageButton(\n                                    Component.literal(\"-\"),\n                                    () -> this.setState(() -> this.count--)\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class FunnySwitchLayout extends StatefulWidget {\n        @Override\n        public WidgetState<FunnySwitchLayout> createState() {\n            return new State();\n        }\n\n        private static class State extends WidgetState<FunnySwitchLayout> {\n            private LayoutAxis axis = LayoutAxis.HORIZONTAL;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Flex(\n                    this.axis,\n                    MainAxisAlignment.START,\n                    CrossAxisAlignment.CENTER,\n                    new MessageButton(\n                        Component.literal(\"switch axis\"),\n                        () -> this.setState(() -> this.axis = this.axis.opposite())\n                    ),\n                    new Padding(Insets.all(5)),\n                    new Panel(\n                        Panel.VANILLA_LIGHT,\n                        new Padding(\n                            Insets.all(10),\n                            new Column(\n                                MainAxisAlignment.START,\n                                CrossAxisAlignment.CENTER,\n                                new Label(Component.literal(\"that's text\")),\n                                new Label(Component.literal(\"some more text\")),\n                                new Padding(\n                                    Insets.top(5),\n                                    new Counter()\n                                )\n                            )\n                        )\n                    ),\n                    new Padding(Insets.all(5)),\n                    new Panel(\n                        Panel.VANILLA_DARK,\n                        new Padding(\n                            Insets.all(10),\n                            new Column(\n                                MainAxisAlignment.START,\n                                CrossAxisAlignment.CENTER,\n                                new Label(Component.literal(\"that's text\")),\n                                new Label(Component.literal(\"some more text\")),\n                                new Padding(\n                                    Insets.top(5),\n                                    new Counter()\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class DragArenaTest extends StatefulWidget {\n        @Override\n        public WidgetState<DragArenaTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<DragArenaTest> {\n\n            private final Set<WindowController> windows = new HashSet<>();\n\n            @Override\n            public Widget build(BuildContext context) {\n                var elements = new ArrayList<Widget>(List.of(\n                    new FunnyDragText(Component.literal(\"me too!\")),\n                    new FunnyDragText(Component.literal(\"drag me!\"))\n                ));\n\n                for (var controller : this.windows) {\n                    elements.add(new Window(\n                        true,\n                        Component.literal(\"window \" + controller.hashCode()),\n                        () -> setState(() -> this.windows.remove(controller)),\n                        controller,\n                        Size.of(150, 75),\n                        new Stack(\n                            new Center(\n                                new AspectRatio(\n                                    16d / 9d,\n                                    new Box(\n                                        Color.WHITE.withA(.5),\n                                        new Label(Component.literal(\"16:9 aspect ratio\"))\n                                    )\n                                )\n                            ),\n                            new Align(\n                                Alignment.TOP_LEFT,\n                                new Column(\n                                    new Label(Component.literal(\"a\").setStyle(Style.EMPTY.withClickEvent(new ClickEvent.OpenUrl(URI.create(\"https://chyz.xyz/box\"))))),\n                                    new MessageButton(Component.literal(\"window button :o\"), () -> setState(() -> controller.toggleCollapsed()))\n                                )\n                            ),\n                            new Align(\n                                Alignment.BOTTOM_RIGHT,\n                                new Tooltip(\n                                    Component.literal(\"tooltip\\nhere?\"),\n                                    new ItemStackWidget(BuiltInRegistries.ITEM.getRandom(RandomSource.create(controller.hashCode())).get().value().getDefaultInstance())\n                                )\n                            ),\n                            new Align(\n                                Alignment.BOTTOM_LEFT,\n                                new BlockWidget(\n                                    BuiltInRegistries.BLOCK.getRandom(RandomSource.create(controller.hashCode())).get().value().defaultBlockState()\n                                )\n                            )\n                        )\n                    ));\n                }\n\n                return new Stack(\n                    new DragArena(elements),\n                    new Align(\n                        Alignment.BOTTOM,\n                        new MessageButton(\n                            Component.literal(\"add window\"),\n                            () -> setState(() -> this.windows.add(new WindowController()))\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class FunnyDragText extends StatefulWidget {\n\n        public final Component text;\n\n        public FunnyDragText(Component text) {\n            this.text = text;\n        }\n\n        @Override\n        public WidgetState<FunnyDragText> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<FunnyDragText> {\n\n            private double x = 0, y = 0;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new DragArenaElement(\n                    this.x,\n                    this.y,\n                    new MouseArea(\n                        widget -> widget\n                            .dragCallback(($, $$, dx, dy) -> this.setState(() -> {\n                                this.x += dx;\n                                this.y += dy;\n                            }))\n                            .cursorStyle(CursorStyle.HAND),\n                        new Panel(\n                            Panel.VANILLA_DARK,\n                            new Padding(\n                                Insets.all(5),\n                                new Label(this.widget().text)\n                            )\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class SplitPaneTest extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            return new Column(\n                MainAxisAlignment.START,\n                CrossAxisAlignment.CENTER,\n                new Sized(\n                    250.0,\n                    200.0,\n                    new MultiSplitPane(\n                        LayoutAxis.HORIZONTAL,\n                        MainAxisAlignment.START,\n                        CrossAxisAlignment.CENTER,\n                        List.of(\n                            new Box(\n                                Color.mix(.5, Color.GREEN, new Color(0)),\n                                new Label(Component.literal(\"text here\"))\n                            ),\n                            new Box(\n                                Color.mix(.5, Color.GREEN, new Color(0)),\n                                new Label(Component.literal(\"text here\"))\n                            ),\n                            new Box(\n                                Color.mix(.5, Color.GREEN, new Color(0)),\n                                new Label(Component.literal(\"text here\"))\n                            ),\n                            new Box(\n                                Color.mix(.5, Color.GREEN, new Color(0)),\n                                new Label(Component.literal(\"text here\"))\n                            )\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    public static class TextInputTest extends StatefulWidget {\n        @Override\n        public WidgetState<TextInputTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<TextInputTest> {\n            private final TextEditingController controller1 = new TextEditingController();\n            private final TextEditingController controller2 = new TextEditingController();\n            private final TextEditingController controller3 = new TextEditingController();\n            private final TextEditingController controller4 = new TextEditingController();\n            private final TextEditingController controller5 = new TextEditingController();\n\n            private Color numbersColor = Color.randomHue();\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Row(\n                    MainAxisAlignment.CENTER,\n                    CrossAxisAlignment.CENTER,\n                    new Padding(Insets.horizontal(10)),\n                    List.of(\n                        new Column(\n                            MainAxisAlignment.START,\n                            CrossAxisAlignment.CENTER,\n                            new Sized(\n                                100.0,\n                                50.0,\n                                new TextBox(\n                                    this.controller1,\n                                    widget -> widget\n                                        .suggestion(Component.literal(\"Soft Wrapping Moment\"))\n                                        .formatter(PatternFormatter.deny(Pattern.compile(\"\\\\*\\\\*\\\\*\\\\*\\\\*\"), \"Penis\"))\n                                        .formatter(PatternFormatter.deny(Pattern.compile(\"\\\\*\\\\*\\\\*\\\\*\"), \"cunt\"))\n                                )\n                            ),\n                            new Sized(\n                                100.0,\n                                50.0,\n                                new TextBox(\n                                    this.controller2,\n                                    widget -> widget\n                                        .softWrap(false)\n                                        .autoFocus(true)\n                                        .placeholder(Component.literal(\"No Soft Wrapping Moment (also auto focused)\"))\n                                )\n                            ),\n                            new Sized(\n                                100.0,\n                                20,\n                                new Focusable(\n                                    widget -> widget\n                                        .skipTraversal(true)\n                                        .keyDownCallback((keyCode, modifiers) -> {\n                                            if (keyCode != GLFW.GLFW_KEY_ENTER || !modifiers.equals(KeyModifiers.NONE)) {\n                                                return false;\n                                            }\n\n                                            this.setState(() -> this.numbersColor = Color.randomHue());\n                                            return true;\n                                        }),\n                                    new TextBox(\n                                        this.controller3,\n                                        widget -> widget\n                                            .baseStyle(Style.EMPTY.withColor(this.numbersColor.argb()))\n                                            .formatter(PatternFormatter.allow(Pattern.compile(\"[0-9]\")))\n                                            .placeholder(Component.literal(\"only numbers\"))\n                                    )\n                                )\n                            ),\n                            new Sized(\n                                100.0,\n                                20.0,\n                                new TextBox(\n                                    this.controller4,\n                                    widget -> widget\n                                        .singleLine()\n                                        .placeholder(Component.literal(\"Single Line Moment\"))\n                                )\n                            ),\n                            new Sized(\n                                100.0,\n                                20.0,\n                                new TextBox(\n                                    this.controller5,\n                                    widget -> widget\n                                        .singleLine()\n                                        .formatter(new MaxLengthFormatter(3))\n                                        .placeholder(Component.literal(\"3 chars, TILI\"))\n                                )\n                            )\n                        ),\n                        new ToggleFest()\n                    )\n                );\n            }\n        }\n\n        public static class ToggleFest extends StatefulWidget {\n            @Override\n            public WidgetState<ToggleFest> createState() {\n                return new State();\n            }\n\n            public static class State extends WidgetState<ToggleFest> {\n\n                private final Entity chyz = EntityComponent.createRenderablePlayer(new GameProfile(\n                    UUID.fromString(\"09de8a6d-86bf-4c15-bb93-ce3384ce4e96\"),\n                    \"chyzman\"\n                ));\n\n                private boolean checked = false;\n\n                @Override\n                public Widget build(BuildContext context) {\n                    return new Column(\n                        MainAxisAlignment.CENTER,\n                        CrossAxisAlignment.START,\n                        new Padding(Insets.vertical(5)),\n                        List.of(\n                            new LabelBox(\n                                new Checkbox(\n                                    new CheckboxStyle(\n                                        active -> new Sized(\n                                            20,\n                                            20,\n                                            new EntityWidget(1.5d, this.chyz, widget -> widget.displayMode(EntityWidget.DisplayMode.CURSOR))\n                                        ),\n                                        EmptyWidget.INSTANCE,\n                                        null\n                                    ), this.checked,\n                                    this::onUpdate\n                                ),\n                                \"chyzbox\"\n                            ),\n                            new LabelBox(\n                                new Checkbox(\n                                    new CheckboxStyle(\n                                        null,\n                                        new Center(new SpriteWidget(new Material(SpriteWidget.GUI_ATLAS_ID, Identifier.fromNamespaceAndPath(\"uwu\", \"czechbox\")))),\n                                        null\n                                    ), this.checked,\n                                    this::onUpdate\n                                ),\n                                this.checked ? \"czechbox\" : \"checkbox\"\n                            ),\n                            new LabelBox(\n                                new Checkbox(this.checked, this::onUpdate),\n                                \"checkbox\"\n                            ),\n                            new LabelBox(\n                                new Checkbox(CheckboxStyle.BRAID, this.checked, this::onUpdate),\n                                \"smolbox\"\n                            )\n                        )\n                    );\n                }\n\n                private void onUpdate(Boolean newState) {\n                    this.setState(() -> {\n                        this.checked = newState;\n                        this.chyz.setSharedFlagOnFire(this.checked);\n                    });\n                }\n            }\n\n            public static class LabelBox extends StatelessWidget {\n                public final Widget widget;\n                public final String label;\n\n                public LabelBox(Widget widget, String label) {\n                    this.widget = widget;\n                    this.label = label;\n                }\n\n                @Override\n                public Widget build(BuildContext context) {\n                    return new Row(\n                        MainAxisAlignment.START,\n                        CrossAxisAlignment.CENTER,\n                        new Padding(Insets.horizontal(4)),\n                        List.of(\n                            new Sized(\n                                20,\n                                20,\n                                new Center(\n                                    this.widget\n                                )\n                            ),\n                            new Label(Component.literal(this.label))\n                        )\n                    );\n                }\n            }\n        }\n    }\n\n    public static class BurningChyzButton extends StatelessWidget {\n        public final Entity chyz;\n        public final Runnable clickCallback;\n\n        public BurningChyzButton(Entity chyz, Runnable clickCallback) {\n            this.chyz = chyz;\n            this.clickCallback = clickCallback;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Clickable(\n                Clickable.alwaysClick(this.clickCallback),\n                new Stack(\n                    new Center(\n                        new Sized(\n                            20.0,\n                            20.0,\n                            new Transform(\n                                new Matrix3x2f().rotation((float) Math.toRadians(90)),\n                                new EntityWidget(\n                                    3.5,\n                                    this.chyz,\n                                    widget -> widget.displayMode(EntityWidget.DisplayMode.CURSOR)\n                                )\n                            )\n                        )\n                    ),\n                    new Label(\n                        LabelStyle.SHADOW,\n                        true,\n                        Component.literal(\"burning chyz\")\n                    )\n                )\n            );\n        }\n    }\n\n    public static class BurningChyzTest extends StatelessWidget {\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Row(\n                MainAxisAlignment.START,\n                CrossAxisAlignment.CENTER,\n                new Padding(Insets.horizontal(10)),\n                List.of(\n                    new Sized(\n                        250.0,\n                        250.0,\n                        new Panel(\n                            Panel.VANILLA_LIGHT,\n                            new Padding(\n                                Insets.all(8),\n                                new Panel(\n                                    Panel.VANILLA_INSET,\n                                    new RecipeViewerStack(\n                                        () -> ViewerStack.OfItem.of(Items.GOLD_BLOCK),\n                                        new StackDropArea(\n                                            stack -> stack instanceof ViewerStack.OfItem,\n                                            stack -> System.out.println(\"chyz: mmm i ate a \" + ((ViewerStack.OfItem) stack).asStack()),\n                                            new RecipeViewerExclusionZone(\n                                                new EntityWidget(\n                                                    1,\n                                                    BurningChyz.of(context),\n                                                    widget -> widget.displayMode(EntityWidget.DisplayMode.CURSOR)\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    ),\n                    new Amogus(\n                        new Box(Color.RED),\n                        new Box(Color.WHITE),\n                        16\n                    ),\n                    new Bikeshed()\n                )\n            );\n        }\n    }\n\n    public static class ScrollTest extends StatefulWidget {\n\n        @Override\n        public WidgetState<?> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<ScrollTest> {\n\n            private final ScrollController horizontalController = new ScrollController(this);\n            private final ScrollController verticalController = new ScrollController(this);\n            private final WindowController controller = new WindowController();\n\n            private final ScrollController horizontalNestedScrollController = new ScrollController(this);\n            private final ScrollController verticalNestedScrollController = new ScrollController(this);\n            private final WindowController nestedScrollController = new WindowController();\n            private double nestedSliderValue = 0.5;\n\n            @Override\n            public void init() {\n                this.controller.setX((Minecraft.getInstance().getWindow().getGuiScaledWidth() - 200) / 2d);\n                this.controller.setY((Minecraft.getInstance().getWindow().getGuiScaledHeight() - 200) / 2d);\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                var text = BraidUtils.fold(\n                    Wisdom.ALL_THE_WISDOM,\n                    Component.empty(),\n                    (result, wisdom) -> {\n                        var wisdomColor = Color.hsv(\n                            new java.util.Random(wisdom.hashCode()).nextFloat(), .75f, 1f\n                        ).rgb();\n\n                        return result.append(\n                            Component.literal(wisdom + \"\\n\").withStyle(style -> style.withColor(wisdomColor))\n                        );\n                    }\n                );\n\n                return new DragArena(\n                    new Window(\n                        false,\n                        Component.literal(\"wisdom, but colored!\"),\n                        null,\n                        this.controller,\n                        Size.square(200),\n                        new Column(\n                            new Flexible(\n                                new Row(\n                                    new Flexible(\n                                        new Scrollable(\n                                            true,\n                                            true,\n                                            this.horizontalController,\n                                            this.verticalController,\n                                            new ScrollAnimationSettings(Duration.ofMillis(2000), Easing.OUT_QUART),\n                                            new Sized(\n                                                500.0,\n                                                null,\n                                                new Label(\n                                                    LabelStyle.SHADOW,\n                                                    true,\n                                                    text\n                                                )\n                                            )\n                                        )\n                                    ),\n                                    new Sized(\n                                        10.0,\n                                        200.0,\n                                        new ListenableBuilder(\n                                            this.verticalController,\n                                            buildContext -> new Slider(\n                                                this.verticalController.offset(),\n                                                widget -> widget\n                                                    .range(this.verticalController.maxOffset(), 0)\n                                                    .vertical(), this.verticalController::jumpTo\n                                            )\n                                        )\n                                    )\n                                )\n                            ),\n                            new Sized(\n                                null,\n                                10.0,\n                                new Row(\n                                    new Flexible(\n                                        new ListenableBuilder(\n                                            this.horizontalController,\n                                            buildContext -> new Slider(\n                                                this.horizontalController.offset(),\n                                                widget -> widget\n                                                    .range(0, this.horizontalController.maxOffset()), this.horizontalController::jumpTo\n                                            )\n                                        )\n                                    ),\n                                    new Padding(Insets.all(5))\n                                )\n                            )\n                        )\n                    ),\n                    new Window(\n                        false,\n                        Component.literal(\"Scrollception\"),\n                        null,\n                        this.nestedScrollController,\n                        Size.square(200),\n                        new Column(\n                            Label.literal(\"Damn bro, you can scroll this?\"),\n                            new Flexible(\n                                new ScrollableWithBars(\n                                    horizontalNestedScrollController,\n                                    verticalNestedScrollController,\n                                    ScrollAnimationSettings.DEFAULT,\n                                    10,\n                                    ButtonScrollbar::new,\n                                    new Sized(\n                                        500, 500,\n                                        new Center(\n                                            new Column(\n                                                MainAxisAlignment.CENTER,\n                                                CrossAxisAlignment.CENTER,\n                                                new Sized(\n                                                    100, 20,\n                                                    new Slider(\n                                                        this.nestedSliderValue,\n                                                        null, value -> this.setState(() -> this.nestedSliderValue = value)\n                                                    )\n                                                ),\n                                                new Sized(\n                                                    20, 100,\n                                                    new Slider(\n                                                        this.nestedSliderValue,\n                                                        Slider::vertical, value -> this.setState(() -> this.nestedSliderValue = value)\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class InputTest extends StatefulWidget {\n        @Override\n        public WidgetState<InputTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<InputTest> {\n            private final List<Component> inputs = Util.make(() -> {\n                var list = new ArrayList<Component>();\n                list.add(Component.literal(\"Help idk how to make this scroll to the bottom when i add shit (everyone laugh at this user)\"));\n                return list;\n            });\n            private final ScrollController controller = new ScrollController(this);\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Sized(\n                    250.0,\n                    null,\n                    new Panel(\n                        Panel.VANILLA_LIGHT,\n                        new Padding(\n                            Insets.all(8),\n                            new Column(\n                                new Label(Component.literal(\"Interact with V this V\")),\n                                new Sized(\n                                    null, 200,\n                                    new MouseArea(\n                                        area -> area.cursorStyle(CursorStyle.HAND)\n                                            .clickCallback((x, y, button, modifiers) -> this.addToList(getMouseButtonName(button).append(\" pressed at:\\n\").append(formatCoordinates(x, y))))\n                                            .releaseCallback((x, y, button, modifiers) -> this.addToList(getMouseButtonName(button).append(\" released at:\\n\").append(formatCoordinates(x, y))))\n                                            .dragStartCallback((button, modifiers) -> this.addToList(getMouseButtonName(button).append(\" drag started\")))\n                                            .dragEndCallback(() -> this.addToList(Component.literal(\"Drag ended\")))\n                                            .enterCallback(() -> this.addToList(Component.literal(\"Mouse entered\")))\n                                            .exitCallback(() -> this.addToList(Component.literal(\"Mouse exited\"))),\n                                        new Focusable(\n                                            input ->\n                                                input.keyDownCallback((key, modifiers) -> this.addToList(getKeyName(key).append(\" pressed\")))\n                                                    .keyUpCallback((key, modifiers) -> this.addToList(getKeyName(key).append(\" released\")))\n                                                    .focusGainedCallback(() -> this.addToList(Component.literal(\"Focus gained\")))\n                                                    .focusLostCallback(() -> this.addToList(Component.literal(\"Focus lost\")))\n                                                    .charCallback((charCode, modifiers) -> this.addToList(Component.literal(\"Character typed: \\\"\" + (char) charCode + \"\\\"\"))),\n                                            new Panel(\n                                                Panel.VANILLA_INSET,\n                                                new VerticallyScrollable(\n                                                    controller,\n                                                    null,\n                                                    new Column(\n                                                        new Padding(Insets.vertical(2)),\n                                                        this.inputs.stream().map(Label::new).toList()\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n\n            private boolean addToList(Component text) {\n                this.setState(() -> {\n                    this.inputs.add(text);\n                    this.schedulePostLayoutCallback(() -> {\n                        this.controller.jumpTo(this.controller.maxOffset());\n                    });\n                });\n                return false; // return false to allow other shit to happen:tm:\n            }\n\n            private MutableComponent getKeyName(int key) {\n                return Component.empty().append(InputConstants.Type.KEYSYM.getOrCreate(key).getDisplayName());\n            }\n\n            private MutableComponent getMouseButtonName(int button) {\n                return Component.empty().append(InputConstants.Type.MOUSE.getOrCreate(button).getDisplayName());\n            }\n\n            private Component formatCoordinates(double x, double y) {\n                return Component.literal(\"[x: \" + formatDouble(x) + \", y: \" + formatDouble(y) + \"]\");\n            }\n        }\n    }\n\n    public static class CyclingTest extends StatefulWidget {\n        private static final List<String> coolStrings = List.of(\n            \"first\", \"second\", \"third\", \"fourth\", \"fifth\"\n        );\n\n        private static final List<Integer> coolNumbers = List.of(\n            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10\n        );\n\n        @Override\n        public WidgetState<CyclingTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<CyclingTest> {\n            private CoolEnum selectedEnum = CoolEnum.FIRST;\n            private CoolEnum selectedEnumNoWrap = CoolEnum.FIRST;\n            private boolean selectedBoolean = false;\n            private boolean selectedBooleanNoWrap = false;\n            private String selectedString = coolStrings.get(0);\n            private String selectedStringNoWrap = coolStrings.get(0);\n            private int selectedInt = 0;\n            private int selectedIntNoWrap = 0;\n\n            private boolean altButtons = true;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Column(\n                    MainAxisAlignment.CENTER,\n                    CrossAxisAlignment.CENTER,\n                    new DefaultButtonStyle(\n                        this.altButtons ? new ButtonStyle(\n                            (active, child) -> new HoverableBuilder(\n                                (hoverableContext, hovered, hoverableChild) -> {\n                                    return new Box(\n                                        active\n                                            ? (hovered || Focusable.levelOf(hoverableContext) == FocusLevel.HIGHLIGHT ? Color.BLUE : Color.WHITE)\n                                            : Color.BLACK,\n                                        true,\n                                        hoverableChild\n                                    );\n                                },\n                                child\n                            ),\n                            Insets.all(10.0),\n                            SoundEvents.GENERIC_EXPLODE.value()\n                        ) : ButtonStyle.DEFAULT,\n                        new Grid(\n                            LayoutAxis.VERTICAL,\n                            4,\n                            Grid.CellFit.tight(),\n                            widget -> new Padding(Insets.all(5), widget),\n                            null,\n                            new Label(Component.literal(\"Cycler\")),\n                            new Label(Component.literal(\"No Wrap\")),\n                            new Label(Component.literal(\"Values\")),\n                            new Label(Component.literal(\"Enum\")),\n                            MessageCyclingButton.forEnum(\n                                this.selectedEnum,\n                                Component.literal(selectedEnum.name()),\n                                (value, index) -> this.setState(() -> this.selectedEnum = value)\n                            ),\n                            MessageCyclingButton.forEnum(\n                                this.selectedEnumNoWrap,\n                                false,\n                                Component.literal(selectedEnumNoWrap.name()),\n                                (value, index) -> this.setState(() -> this.selectedEnumNoWrap = value)\n                            ),\n                            new Label(Component.literal(String.join(\", \", Arrays.stream(CoolEnum.values()).map(Enum::name).collect(Collectors.toList())))),\n                            new Label(Component.literal(\"Boolean\")),\n                            MessageCyclingButton.forBoolean(\n                                this.selectedBoolean,\n                                Component.literal(this.selectedBoolean ? \"true\" : \"false\"),\n                                (value, index) -> this.setState(() -> this.selectedBoolean = value)\n                            ),\n                            new MessageCyclingButton<>(\n                                List.of(false, true), this.selectedBooleanNoWrap, false,\n                                Component.literal(this.selectedBooleanNoWrap ? \"true\" : \"false\"),\n                                (value, index) -> this.setState(() -> this.selectedBooleanNoWrap = value)\n                            ),\n                            new Label(Component.literal(\"false, true\")),\n                            new Label(Component.literal(\"String\")),\n                            new MessageCyclingButton<>(\n                                coolStrings,\n                                this.selectedString,\n                                Component.literal(this.selectedString),\n                                (value, index) -> this.setState(() -> this.selectedString = value)\n                            ),\n                            new MessageCyclingButton<>(\n                                coolStrings,\n                                this.selectedStringNoWrap,\n                                false,\n                                Component.literal(this.selectedStringNoWrap),\n                                (value, index) -> this.setState(() -> this.selectedStringNoWrap = value)\n                            ),\n                            new Label(Component.literal(String.join(\", \", coolStrings))),\n                            new Label(Component.literal(\"Int\")),\n                            new MessageCyclingButton<>(\n                                coolNumbers,\n                                this.selectedInt,\n                                Component.literal(coolNumbers.get(this.selectedInt).toString()),\n                                (value, index) -> this.setState(() -> this.selectedInt = index)\n                            ),\n                            new MessageCyclingButton<>(\n                                coolNumbers,\n                                this.selectedIntNoWrap,\n                                false,\n                                Component.literal(coolNumbers.get(this.selectedIntNoWrap).toString()),\n                                (value, index) -> this.setState(() -> this.selectedIntNoWrap = index)\n                            ),\n                            new Label(\n                                Component.literal(coolNumbers.stream()\n                                    .map(String::valueOf)\n                                    .collect(Collectors.joining(\", \")))\n                            )\n                        )\n                    ),\n                    new Row(\n                        MainAxisAlignment.CENTER,\n                        CrossAxisAlignment.CENTER,\n                        new Checkbox(CheckboxStyle.BRAID, this.altButtons, nowChecked -> this.setState(() -> this.altButtons = nowChecked)),\n                        new Padding(\n                            Insets.left(5),\n                            Label.literal(\"alternate buttons\")\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public enum CoolEnum {\n        FIRST,\n        SECOND,\n        THIRD,\n        FOURTH,\n        FIFTH\n    }\n\n    public static class VanillaTest extends StatefulWidget {\n        @Override\n        public WidgetState<VanillaTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<VanillaTest> {\n            @Override\n            public Widget build(BuildContext context) {\n                return new Sized(\n                    250.0,\n                    250.0,\n                    new Column(\n                        new VanillaWidget<>(\n                            Size.of(250, 20),\n                            () ->\n                                net.minecraft.client.gui.components.Checkbox.builder(\n                                    Component.literal(\"Checkbox\"),\n                                    Minecraft.getInstance().font\n                                ).build()\n                        ),\n                        new VanillaWidget<>(\n                            Size.of(250, 20),\n                            () -> {\n                                var widget = new EditBox(\n                                    Minecraft.getInstance().font,\n                                    0, 0, 100, 20,\n                                    Component.literal(\"Text Field\")\n                                );\n                                widget.setHint(Component.literal(\"when the vanilla widget is no longer better than the braid widget\"));\n                                return widget;\n                            }\n                        ),\n                        new OwoUIWidget(\n                            () -> {\n                                var root = UIContainers.verticalFlow(Sizing.content(), Sizing.content());\n                                root.child(\n                                    UIComponents.button(\n                                        Component.literal(\"A very very cool button\"),\n                                        button -> Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.GENERIC_EXPLODE, RandomSource.create().nextFloat() * 2f))\n                                    )\n                                ).child(\n                                    new BraidComponent(\n                                        new Column(\n                                            new MessageButton(\n                                                Component.literal(\"amogus\"),\n                                                () -> Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.ANVIL_BREAK, RandomSource.create().nextFloat() * 2f))\n                                            ),\n                                            // idk why this needs to be here but if it's not the split pane becomes\n                                            // infinity sized\n                                            new Sized(\n                                                180, 180,\n                                                new MultiSplitPane(\n                                                    LayoutAxis.HORIZONTAL,\n                                                    MainAxisAlignment.START,\n                                                    CrossAxisAlignment.CENTER,\n                                                    List.of(\n                                                        new Box(\n                                                            Color.mix(.5, Color.GREEN, new Color(0)),\n                                                            new Label(Component.literal(\"no way is\"))\n                                                        ),\n                                                        new Box(\n                                                            Color.mix(.5, Color.GREEN, new Color(0)),\n                                                            new Label(Component.literal(\"that braid\"))\n                                                        ),\n                                                        new Box(\n                                                            Color.mix(.5, Color.GREEN, new Color(0)),\n                                                            new Label(Component.literal(\"inside owoui\"))\n                                                        ),\n                                                        new Box(\n                                                            Color.mix(.5, Color.GREEN, new Color(0)),\n                                                            new Label(Component.literal(\"inside braid?\"))\n                                                        )\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    ).sizing(Sizing.fixed(200))\n                                ).allowOverflow(true);\n                                return root;\n                            }\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class SharedStateTest extends StatefulWidget {\n        @Override\n        public WidgetState<SharedStateTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<SharedStateTest> {\n            @Override\n            public Widget build(BuildContext context) {\n                return new Sized(\n                    400,\n                    250,\n                    new Column(\n                        new Flexible(new TheTest(false)),\n                        new Flexible(new TheTest(true))\n                    )\n                );\n            }\n\n            public static class TheTest extends StatelessWidget {\n\n                public final boolean nest;\n\n                public TheTest(boolean nest) {\n                    this.nest = nest;\n                }\n\n                @Override\n                public Widget build(BuildContext context) {\n                    return new SharedState<>(\n                        CounterState::new,\n                        new Row(\n                            new Flexible(new LeftBody()),\n                            new Flexible(new Center(new RightBody())),\n                            this.nest ? new Flexible(2, new TheTest(false)) : new Padding(Insets.none())\n                        )\n                    );\n                }\n            }\n\n            public static class LeftBody extends StatelessWidget {\n                @Override\n                public Widget build(BuildContext context) {\n                    System.out.println(\"panel rebuild\");\n                    return new Panel(\n                        SharedState.select(context, CounterState.class, state -> state.dark)\n                            ? Panel.VANILLA_DARK\n                            : Panel.VANILLA_LIGHT,\n                        new CounterText()\n                    );\n                }\n            }\n\n            public static class RightBody extends StatelessWidget {\n                @Override\n                public Widget build(BuildContext context) {\n                    return new IntrinsicWidth(\n                        new Column(\n                            new Button(\n                                () -> SharedState.set(context, CounterState.class, state -> state.count += 1),\n                                new Label(Component.literal(\"increment\"))\n                            ),\n                            new Button(\n                                () -> SharedState.set(context, CounterState.class, state -> state.dark = !state.dark),\n                                new Label(Component.literal(\"toggle darkness\"))\n                            )\n                        )\n                    );\n                }\n            }\n\n            public static class CounterText extends StatelessWidget {\n                @Override\n                public Widget build(BuildContext context) {\n                    System.out.println(\"text rebuild\");\n                    return new Label(Component.literal(\"current state: \" + SharedState.select(context, CounterState.class, state -> state.count)));\n                }\n            }\n        }\n\n        public static class CounterState extends ShareableState {\n            public int count = 0;\n            public boolean dark = false;\n        }\n    }\n\n    public static class StacksTest extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            return new Center(\n                new Row(\n                    new Stack(\n                        new Panel(Panel.VANILLA_LIGHT),\n                        new StackBase(new Sized(100, 100, new Padding(Insets.none()))),\n                        new Label(new LabelStyle(Alignment.BOTTOM_RIGHT, null, null, null), true, Component.literal(\"based corner text\"))\n                    ),\n                    new Padding(Insets.horizontal(20)),\n                    new Stack(\n                        new Sized(100, 100, new Panel(Panel.VANILLA_LIGHT)),\n                        new Label(new LabelStyle(Alignment.BOTTOM_RIGHT, null, null, null), true, Component.literal(\"failed corner text\"))\n                    ),\n                    new Padding(Insets.horizontal(20)),\n                    new IntrinsicWidth(\n                        new IntrinsicHeight(\n                            new Stack(\n                                new Sized(100, 100, new Panel(Panel.VANILLA_LIGHT)),\n                                new Label(new LabelStyle(Alignment.BOTTOM_RIGHT, null, null, null), true, Component.literal(\"intrinsic corner text\"))\n                            )\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    public static class GridsTest extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            var random = new java.util.Random(0);\n\n            return new Center(\n                new Grid(\n                    LayoutAxis.VERTICAL,\n                    2,\n                    Grid.CellFit.loose(),\n                    widget -> new Padding(Insets.all(10), widget),\n                    new Sized(\n                        90,\n                        null,\n                        new Grid(\n                            LayoutAxis.VERTICAL,\n                            3,\n                            Grid.CellFit.loose(Alignment.BOTTOM_RIGHT),\n                            new Sized(random.nextInt(15, 31), random.nextInt(15, 31), new Box(nextColor(random))),\n                            new Sized(random.nextInt(15, 31), random.nextInt(15, 31), new Box(nextColor(random))),\n                            new Sized(random.nextInt(15, 31), random.nextInt(15, 31), new Box(nextColor(random))),\n                            new Sized(random.nextInt(15, 31), random.nextInt(15, 31), new Box(nextColor(random))),\n                            new Sized(random.nextInt(15, 31), random.nextInt(15, 31), new Box(nextColor(random)))\n                        )\n                    ),\n                    new IntrinsicWidth(\n                        new Column(\n                            MainAxisAlignment.CENTER,\n                            CrossAxisAlignment.CENTER,\n                            new Grid(\n                                LayoutAxis.VERTICAL,\n                                2,\n                                Grid.CellFit.loose(),\n                                new Sized(20, 40, new Box(Color.WHITE)),\n                                new Sized(20, 20, new Box(Color.WHITE)),\n                                new Sized(20, 20, new Box(Color.WHITE)),\n                                new Sized(60, 40, new Box(Color.WHITE))\n                            ),\n                            new Button(() -> {}, new Label(Component.literal(\"a\")))\n                        )\n                    ),\n                    new IntrinsicWidth(\n                        new Column(\n                            MainAxisAlignment.CENTER,\n                            CrossAxisAlignment.CENTER,\n                            new Grid(\n                                LayoutAxis.VERTICAL,\n                                2,\n                                Grid.CellFit.loose(),\n                                new Sized(40, 40, new Box(Color.WHITE)),\n                                new Sized(20, 20, new Box(Color.WHITE)),\n                                new Sized(20, 20, new Box(Color.WHITE)),\n                                new Sized(40, 40, new Box(Color.WHITE))\n                            ),\n                            new Button(() -> {}, new Label(Component.literal(\"a\")))\n                        )\n                    )\n                )\n            );\n        }\n\n        private static Color nextColor(java.util.Random random) {\n            return Color.hsv(random.nextFloat(), .75f, 1f);\n        }\n    }\n\n    public static class ContributorsTest extends StatefulWidget {\n        @Override\n        public WidgetState<ContributorsTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<ContributorsTest> {\n\n            private List<Contributor> contributors = this.genContributors();\n\n            private List<Contributor> genContributors() {\n                return List.of(\n                    new Contributor(UUID.fromString(\"b6c2d403-bf7c-4e19-b7a2-f64c9e44e56a\"), \"glisco\", Component.translatable(\"text.uwu.glisco\")),\n                    new Contributor(UUID.fromString(\"09de8a6d-86bf-4c15-bb93-ce3384ce4e96\"), \"chyzman\", Component.translatable(\"text.uwu.chyz\")),\n                    new Contributor(UUID.fromString(\"517253c6-5ae6-4a70-8e8f-b8515321f774\"), \"Dragon_Seeker\", TextOps.withColor(\"blodhgarm\", 0xae0000)),\n                    new Contributor(UUID.fromString(\"63db48b4-723a-4323-8d67-45679507fd82\"), \"GreatGrayOwl\", TextOps.withColor(\"skibediah fœtus\", 0x9b57d0)),\n                    new Contributor(UUID.fromString(\"91a033f7-1dd3-4858-9c7b-8fb61ba6363d\"), \"Noaaan\", Component.literal(\"no\" + \"a\".repeat((int) (1 + Math.random() * 7)) + \"n\"))\n                );\n            }\n\n            @Override\n            public Widget build(BuildContext notContext) {\n                return new SharedState<>(\n                    MurderState::new,\n                    new Builder(context -> {\n                        var murders = SharedState.get(context, MurderState.class).murders;\n                        var eepies = SharedState.get(context, MurderState.class).eepies;\n                        var bed = SharedState.get(context, MurderState.class).bed;\n                        return new Column(\n                            MainAxisAlignment.CENTER,\n                            CrossAxisAlignment.CENTER,\n                            new Padding(Insets.all(10)),\n                            new Label(murders.compareTo(BigInteger.ZERO) > 0 ? Component.literal(\"You have committed \" + murders + \" act\" + (murders.compareTo(BigInteger.ONE) > 0 ? \"s\" : \"\") + \" of \" + Component.translatableEscape(\"uwu.homicide\").getString() + \" against the owo contributors!\" + (murders.compareTo(BigInteger.valueOf(1000)) > 0 ? \"... wtf bro\" : \"\")).withColor(CommonColors.RED) : Component.literal(\"OWO Contributors\")),\n                            new Label(eepies.compareTo(BigInteger.ZERO) > 0 ? Component.literal(\"You have committed \" + eepies + \" act\" + (eepies.compareTo(BigInteger.ONE) > 0 ? \"s\" : \"\") + \" of \" + Component.translatableEscape(\"uwu.eepy\").getString() + \" against the owo contributors!\" + (murders.compareTo(BigInteger.valueOf(1000)) > 0 ? \"... idk\" : \"\")).withColor(((BlockItem) bed.getItem()).getBlock().defaultBlockState().getMapColor(Minecraft.getInstance().level, BlockPos.ZERO).col) : Component.empty()),\n                            new Grid(\n                                LayoutAxis.VERTICAL,\n                                3,\n                                Grid.CellFit.loose(),\n                                Stream.concat(\n                                        this.contributors.stream()\n                                            .map(contributor -> {\n                                                return new Padding(\n                                                    Insets.all(8),\n                                                    new Panel(\n                                                        Panel.VANILLA_LIGHT,\n                                                        new Padding(\n                                                            Insets.all(8),\n                                                            new Column(\n                                                                MainAxisAlignment.CENTER,\n                                                                CrossAxisAlignment.CENTER,\n                                                                new Padding(Insets.top(4)),\n                                                                List.of(\n                                                                    new FirePlayer(new GameProfile(contributor.uuid, contributor.name)),\n                                                                    new Label(\n                                                                        LabelStyle.SHADOW,\n                                                                        true,\n                                                                        contributor.displayName().copy().setStyle(contributor.displayName.copy().getStyle().withHoverEvent(new HoverEvent.ShowEntity(new HoverEvent.EntityTooltipInfo(EntityType.PLAYER, contributor.uuid, contributor.displayName))))\n                                                                    ),\n                                                                    new RatingBar()\n                                                                )\n                                                            )\n                                                        )\n                                                    )\n                                                );\n                                            }),\n                                        Stream.of(\n                                            new Sized(\n                                                20,\n                                                20,\n                                                new Button(\n                                                    () -> setState(() -> this.contributors = this.genContributors()),\n                                                    new Label(LabelStyle.SHADOW, true, Component.literal(\"☠\"))\n                                                )\n                                            )\n                                        )\n                                    )\n                                    .toList()\n                            )\n                        );\n                    })\n                );\n            }\n\n            public static class MurderState extends ShareableState {\n                public BigInteger murders = BigInteger.ZERO;\n                public BigInteger eepies = BigInteger.ZERO;\n                private ItemStack bed = UwuItems.BRAID.getDefaultInstance();\n            }\n\n            public record Contributor(UUID uuid, String name, Component displayName) {}\n\n            public static class FirePlayer extends StatefulWidget {\n\n                public final GameProfile profile;\n\n                public FirePlayer(GameProfile profile) {this.profile = profile;}\n\n                @Override\n                public WidgetState<FirePlayer> createState() {\n                    return new FirePlayerState();\n                }\n\n                public static class FirePlayerState extends WidgetState<FirePlayer> {\n\n                    private LivingEntity displayEntity;\n\n                    private boolean dead = false;\n\n                    @Override\n                    public void init() {\n                        this.displayEntity = EntityComponent.createRenderablePlayer(this.widget().profile);\n                    }\n\n                    @Override\n                    public Widget build(BuildContext context) {\n                        if (this.dead) this.displayEntity.setSharedFlagOnFire(false);\n                        this.displayEntity.setHealth(dead ? 0 : 20);\n                        this.displayEntity.deathTime = dead ? 20 : 0;\n                        return Interactable.primary(\n                            this.dead ? null : () -> {\n                                this.setState(() -> {\n                                    this.dead = true;\n                                });\n                                Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.PLAYER_DEATH, 1));\n                                SharedState.set(context, MurderState.class, state -> {\n                                    if (!displayEntity.getUUID().equals(UUID.fromString(\"91a033f7-1dd3-4858-9c7b-8fb61ba6363d\"))) {\n                                        state.murders = state.murders.add(BigInteger.ONE);\n                                    } else {\n                                        state.eepies = state.eepies.add(BigInteger.ONE);\n                                        state.bed = BuiltInRegistries.ITEM.getRandomElementOf(ItemTags.BEDS, RandomSource.create()).get().value().getDefaultInstance();\n                                    }\n                                });\n                                scheduleDelayedCallback(\n                                    Duration.ofSeconds(displayEntity.getUUID().equals(UUID.fromString(\"09de8a6d-86bf-4c15-bb93-ce3384ce4e96\")) ? 1 : 3), () -> this.setState(() -> {\n                                        this.dead = false;\n                                        Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.TOTEM_USE, 1));\n                                    })\n                                );\n                            },\n                            widget -> widget\n                                .enterCallback(!this.dead ? () -> this.displayEntity.setSharedFlagOnFire(true) : null)\n                                .exitCallback(!this.dead ? () -> this.displayEntity.setSharedFlagOnFire(false) : null)\n                                .cursorStyle(!this.dead ? CursorStyle.CROSSHAIR : null),\n                            new Stack(\n                                new StackBase(\n                                    new Panel(\n                                        Identifier.fromNamespaceAndPath(\"uwu\", \"contributors_panel\"),\n                                        new Padding(\n                                            Insets.top(8),\n                                            new Sized(\n                                                96,\n                                                96,\n                                                new EntityWidget(1.35, this.displayEntity, widget -> {\n                                                    widget.displayMode(displayEntity.isDeadOrDying() ? EntityWidget.DisplayMode.NONE : EntityWidget.DisplayMode.CURSOR);\n                                                    if (displayEntity.isDeadOrDying()) {\n                                                        widget.transform((matrix) -> {\n                                                            if (displayEntity.getUUID().equals(UUID.fromString(\"91a033f7-1dd3-4858-9c7b-8fb61ba6363d\"))) {\n                                                                matrix.translate(-1f, 1f, 0);\n                                                            }\n\n                                                            matrix.rotateX((float) Math.toRadians(0.01));\n                                                        });\n                                                    }\n                                                })\n                                            )\n                                        )\n                                    )\n                                ),\n                                new Visibility(\n                                    this.dead && this.displayEntity.getUUID().equals(UUID.fromString(\"91a033f7-1dd3-4858-9c7b-8fb61ba6363d\")),\n                                    new Stack(\n                                        new Transform(\n                                            new Matrix3x2f(),\n                                            new ItemStackWidget(\n                                                SharedState.getWithoutDependency(context, MurderState.class).bed,\n                                                widget -> widget\n                                                    .displayContext(ItemDisplayContext.NONE)\n                                                    .transform(matrix4f -> matrix4f\n                                                        .rotate(Axis.YP.rotationDegrees(90))\n                                                        .rotate(Axis.ZP.rotationDegrees(15))\n                                                        .scale(.45f, .45f, .45f)\n                                                        .translate(0, -.45f, .45f))\n                                            )\n                                        ),\n                                        new Align(\n                                            Alignment.TOP_LEFT,\n                                            new Transform(\n                                                new Matrix3x2f().translation(75, 10),\n                                                new Label(new LabelStyle(Alignment.TOP_LEFT, null, null, true), true, Component.literal(\"    z\\n  z\\nz\"))\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        );\n                    }\n                }\n            }\n\n            public static class RatingBar extends StatefulWidget {\n                @Override\n                public WidgetState<RatingBar> createState() {\n                    return new RatingBarState();\n                }\n\n                public static class RatingBarState extends WidgetState<RatingBar> {\n\n                    private int selectedStarCount = 0;\n                    private int hoverStarCount = 0;\n\n                    @Override\n                    public Widget build(BuildContext context) {\n                        return new MouseArea(\n                            widget -> widget\n                                .exitCallback(() -> setState(() -> this.hoverStarCount = 0))\n                                .cursorStyle(CursorStyle.HAND),\n                            new Row(\n                                this.star(0),\n                                this.star(1),\n                                this.star(2),\n                                this.star(3),\n                                this.star(4)\n                            )\n                        );\n                    }\n\n                    private Widget star(int idx) {\n                        return new Interactable(\n                            SHORTCUTS,\n                            widget -> widget\n                                .addCallbackAction(PrimaryActionIntent.class, ($, $$) -> setState(() -> this.selectedStarCount = idx + 1))\n                                .addCallbackAction(SecondaryActionIntent.class, ($, $$) -> setState(() -> this.selectedStarCount = 0))\n                                .enterCallback(() -> setState(() -> this.hoverStarCount = idx + 1)),\n                            new Stack(\n                                new SpriteWidget(\n                                    new Material(\n                                        Identifier.parse(\"textures/atlas/gui.png\"),\n                                        Identifier.fromNamespaceAndPath(\"uwu\", (idx + 1) <= this.selectedStarCount ? \"favorite_icon_selected\" : \"favorite_icon\")\n                                    )\n                                ),\n                                (idx + 1) <= this.hoverStarCount\n                                    ? new SpriteWidget(\n                                    new Material(\n                                        Identifier.parse(\"textures/atlas/gui.png\"),\n                                        Identifier.fromNamespaceAndPath(\"uwu\", \"favorite_icon_hover\")\n                                    )\n                                ) : new Padding(Insets.none())\n                            )\n                        );\n                    }\n                }\n            }\n        }\n\n        private static final Map<List<ShortcutTrigger>, Intent> SHORTCUTS = Map.of(\n            List.of(ShortcutTrigger.LEFT_CLICK), PrimaryActionIntent.INSTANCE,\n            List.of(ShortcutTrigger.RIGHT_CLICK), SecondaryActionIntent.INSTANCE\n        );\n    }\n\n    public static class AnimationsTest extends StatefulWidget {\n        @Override\n        public WidgetState<AnimationsTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<AnimationsTest> {\n\n            private boolean end = false;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Row(\n                    MainAxisAlignment.START,\n                    CrossAxisAlignment.CENTER,\n                    new Sized(\n                        250,\n                        250,\n                        new Align(\n                            Alignment.TOP,\n                            new Column(\n                                new AnimatedAlign(\n                                    Duration.ofMillis(250),\n                                    Easing.IN_OUT_EXPO,\n                                    this.end ? Alignment.RIGHT : Alignment.CENTER,\n                                    new MessageButton(Component.literal(\"toggle\"), () -> this.setState(() -> this.end = !this.end))\n                                ),\n                                new Box(\n                                    Color.WHITE,\n                                    new AnimatedPadding(\n                                        Duration.ofMillis(500),\n                                        Easing.IN_OUT_EXPO,\n                                        this.end ? Insets.of(0, 50, 50, 50) : Insets.none(),\n                                        new Sized(\n                                            30,\n                                            30,\n                                            new AnimatedBox(\n                                                Duration.ofMillis(500),\n                                                Easing.IN_OUT_QUAD,\n                                                this.end ? Color.RED : Color.GREEN\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    ),\n                    new ManualAnimation(\n                        new Amogus(\n                            new Box(Color.BLUE),\n                            new Box(Color.WHITE),\n                            8\n                        )\n                    )\n                );\n            }\n        }\n\n        public static class ManualAnimation extends StatefulWidget {\n\n            public final Widget child;\n\n            public ManualAnimation(Widget child) {\n                this.child = child;\n            }\n\n            @Override\n            public WidgetState<ManualAnimation> createState() {\n                return new State();\n            }\n\n            public static class State extends WidgetState<ManualAnimation> {\n\n                private static final AlignmentLerp ALIGNMENT_LERP = new AlignmentLerp(Alignment.LEFT, Alignment.RIGHT);\n\n                private Animation.Target currentTarget;\n                private Animation animation;\n                private Alignment alignment;\n\n                @Override\n                public void init() {\n                    this.currentTarget = Animation.Target.START;\n                    this.animation = new Animation(\n                        Easing.OUT_BOUNCE,\n                        Duration.ofMillis(500),\n                        this::scheduleAnimationCallback,\n                        this::onAnimationTick,\n                        this.currentTarget\n                    );\n\n                    this.onAnimationTick(this.animation.progress());\n                }\n\n                private void onAnimationTick(double progress) {\n                    this.setState(() -> this.alignment = ALIGNMENT_LERP.compute(progress));\n                }\n\n                @Override\n                public Widget build(BuildContext context) {\n                    return new Column(\n                        MainAxisAlignment.START,\n                        CrossAxisAlignment.CENTER,\n                        new Sized(\n                            128,\n                            null,\n                            new Box(\n                                Color.WHITE.withA(.5),\n                                true,\n                                new Padding(\n                                    Insets.all(1),\n                                    new Align(\n                                        this.alignment,\n                                        this.widget().child\n                                    )\n                                )\n                            )\n                        ),\n                        new Padding(Insets.vertical(5)),\n                        new MessageButton(\n                            Component.literal(\"toggle\"),\n                            () -> {\n                                var next = this.currentTarget == Animation.Target.START ? Animation.Target.END : Animation.Target.START;\n                                this.currentTarget = next;\n\n                                this.animation.towards(next, false);\n                            }\n                        )\n                    );\n                }\n            }\n        }\n    }\n\n    public static class Amogus extends StatelessWidget {\n\n        public final Widget bodyPixel;\n        public final Widget visorPixel;\n        public final double pixelSize;\n\n        public Amogus(Widget bodyPixel, Widget visorPixel, double pixelSize) {\n            this.bodyPixel = bodyPixel;\n            this.visorPixel = visorPixel;\n            this.pixelSize = pixelSize;\n        }\n\n        @Override\n        public Widget build(BuildContext context) {\n            return new Sized(\n                this.pixelSize * 4,\n                this.pixelSize * 4,\n                new Grid(\n                    LayoutAxis.VERTICAL,\n                    4,\n                    Grid.CellFit.tight(),\n                    (Widget) null,\n                    this.bodyPixel,\n                    this.bodyPixel,\n                    this.bodyPixel,\n                    this.bodyPixel,\n                    this.bodyPixel,\n                    this.visorPixel,\n                    this.visorPixel,\n                    this.bodyPixel,\n                    this.bodyPixel,\n                    this.bodyPixel,\n                    this.bodyPixel,\n                    null,\n                    this.bodyPixel,\n                    null,\n                    this.bodyPixel\n                )\n            );\n        }\n    }\n\n    public static class NavigatorTest extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            return new Center(\n                new Sized(\n                    150, 150,\n                    new Box(\n                        Color.BLACK, true,\n                        new Padding(\n                            Insets.all(1),\n                            new Center(new Navigator(new Page1()))\n                        )\n                    )\n                )\n            );\n        }\n\n        public static class Page1 extends StatelessWidget {\n            @Override\n            public Widget build(BuildContext context) {\n                return new Column(\n                    new MessageButton(\n                        Component.literal(\"page 1\"),\n                        () -> Navigator.push(context, new BasePage(new Label(Component.literal(\"page 1\"))))\n                    ),\n                    new MessageButton(\n                        Component.literal(\"page 2\"),\n                        () -> Navigator.push(\n                            context, new BasePage(\n                                new Column(\n                                    new Label(Component.literal(\"page 2\")),\n                                    new MessageButton(\n                                        Component.literal(\"popup\"),\n                                        () -> Navigator.pushOverlay(\n                                            context, new Dialog(\n                                                new Panel(\n                                                    Panel.VANILLA_LIGHT,\n                                                    new Padding(\n                                                        Insets.all(5),\n                                                        new Sized(\n                                                            Size.square(64),\n                                                            new Bikeshed()\n                                                        )\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n        }\n\n        public static class BasePage extends StatelessWidget {\n\n            public final Widget content;\n\n            public BasePage(Widget content) {\n                this.content = content;\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Column(\n                    MainAxisAlignment.START,\n                    CrossAxisAlignment.CENTER,\n                    this.content,\n                    new Padding(\n                        Insets.top(10),\n                        new MessageButton(Component.literal(\"go back\"), () -> Navigator.pop(context))\n                    )\n                );\n            }\n        }\n    }\n\n    public static class OverlayTest extends StatefulWidget {\n        @Override\n        public WidgetState<OverlayTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<OverlayTest> {\n\n            private @Nullable Tests selectedOption = null;\n\n            private void spawn(BuildContext context, double x, double y) {\n                Overlay.of(context).add(\n                    new OverlayEntryBuilder(\n                        new Amogus(\n                            new Box(Color.randomHue()),\n                            new Box(Color.WHITE),\n                            8\n                        ),\n                        new RelativePosition(context, x - 12, y - 12)\n                    )\n                );\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Overlay(\n                    new Builder(innerContext -> {\n                        return new Sized(\n                            Double.POSITIVE_INFINITY,\n                            Double.POSITIVE_INFINITY,\n                            new MouseArea(\n                                widget -> widget\n                                    .clickCallback((x, y, button, modifiers) -> {\n                                        this.spawn(innerContext, x, y);\n                                        return true;\n                                    })\n                                    .dragCallback((x, y, dx, dy) -> {\n                                        this.spawn(innerContext, x, y);\n                                    }),\n                                new Center(\n                                    new Panel(\n                                        Panel.VANILLA_LIGHT,\n                                        new Padding(\n                                            Insets.all(10),\n                                            new Sized(\n                                                120,\n                                                null,\n                                                new ComboBox<>(\n                                                    test -> Component.literal(test.name().toLowerCase(Locale.ROOT).replace('_', ' ')),\n                                                    Arrays.asList(Tests.values()),\n                                                    this.selectedOption,\n                                                    option -> this.setState(() -> this.selectedOption = option)\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        );\n                    })\n                );\n            }\n        }\n    }\n\n    public static class TextTest extends StatefulWidget {\n        @Override\n        public WidgetState<TextTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<TextTest> {\n\n            @Override\n            public Widget build(BuildContext context) {\n                var wisdomText = Component.literal(String.join(\" \", Wisdom.ALL_THE_WISDOM));\n\n                return new DragArena(\n                    new Window(\n                        false,\n                        Component.literal(\"ellipsis moment\"),\n                        null, null,\n                        Size.square(100),\n                        new Label(\n                            new LabelStyle(Alignment.TOP_LEFT, null, null, null),\n                            true, Label.Overflow.ELLIPSIS,\n                            wisdomText\n                        )\n                    ),\n                    new Window(\n                        false,\n                        Component.literal(\"clip moment\"),\n                        null, null,\n                        Size.square(100),\n                        new Label(\n                            new LabelStyle(Alignment.TOP_LEFT, null, null, null),\n                            true, Label.Overflow.CLIP,\n                            wisdomText\n                        )\n                    ),\n                    new Window(\n                        false,\n                        Component.literal(\"marquee moment\"),\n                        null, null,\n                        Size.square(100),\n                        new Column(\n                            Wisdom.ALL_THE_WISDOM.stream()\n                                .sorted(Comparator.comparingInt(value -> Minecraft.getInstance().font.width(value)))\n                                .map(s -> new Marquee(\n                                    new Label(\n                                        new LabelStyle(Alignment.TOP_LEFT, null, null, null),\n                                        true, Label.Overflow.CLIP,\n                                        Component.literal(s)\n                                    )\n                                )).toList()\n                        )\n                    ),\n                    new Window(\n                        false,\n                        Component.literal(\"cursed marquee moment\"),\n                        null, null,\n                        Size.square(100),\n                        new Marquee(\n                            widget -> widget.pauseWhileHovered(false),\n                            new Bikeshed()\n                        )\n                    ),\n                    new Window(\n                        false,\n                        Component.literal(\"dvd moment\"),\n                        null, null,\n                        Size.square(100),\n                        new Center(\n                            new Marquee(\n                                widget -> widget.axis(LayoutAxis.VERTICAL),\n                                new Marquee(\n                                    new Panel(\n                                        Identifier.fromNamespaceAndPath(\"uwu\", \"contributors_panel\"),\n                                        new Sized(\n                                            32 * 4,\n                                            32 * 4,\n                                            new Marquee(\n                                                widget -> widget\n                                                    .easing(Easing.LINEAR)\n                                                    .minDuration(0)\n                                                    .durationPerPixel(10)\n                                                    .pauseTime(0),\n                                                new Marquee(\n                                                    widget -> widget\n                                                        .easing(Easing.LINEAR)\n                                                        .minDuration(0)\n                                                        .durationPerPixel(15)\n                                                        .pauseTime(0)\n                                                        .axis(LayoutAxis.VERTICAL),\n                                                    new Align(\n                                                        Alignment.TOP_LEFT,\n                                                        new Padding(\n                                                            Insets.all(32 * 3),\n                                                            new GayAmogus(\n                                                                8\n                                                            )\n                                                        )\n                                                    )\n                                                )\n                                            )\n                                        )\n                                    )\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n        }\n    }\n\n    public static class GayAmogus extends StatefulWidget {\n\n        public final double pixelSize;\n        public GayAmogus(double pixelSize) {\n            this.pixelSize = pixelSize;\n        }\n\n        @Override\n        public WidgetState<GayAmogus> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<GayAmogus> {\n\n            @Override\n            public void init() {\n                this.update(Duration.ZERO);\n            }\n\n            private void update(Duration delta) {\n                this.setState(() -> {});\n                this.scheduleAnimationCallback(this::update);\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Amogus(\n                    new Box(Color.hsv(System.currentTimeMillis() / 5000d % 1d, .85, 1)),\n                    new Box(Color.WHITE),\n                    this.widget().pixelSize\n                );\n            }\n        }\n    }\n\n    public static class SpinnyGhastTest extends StatefulWidget {\n        @Override\n        public WidgetState<SpinnyGhastTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<SpinnyGhastTest> {\n\n            private List<Entity> entities;\n            private int selectedEntityIdx = 0;\n\n            private double pitch = 35;\n            private double pitchDelta = 0;\n\n            private double yaw = -45;\n            private double yawDelta = 0;\n\n            private boolean releasedLate = false;\n\n            @Override\n            public void init() {\n                this.entities = Stream.of(\n                    EntityType.HAPPY_GHAST,\n                    EntityType.ALLAY,\n                    EntityType.COW,\n                    EntityType.CREAKING,\n                    EntityType.BREEZE,\n                    EntityType.COPPER_GOLEM\n                ).<Entity>map(\n                    entityType -> entityType.create(Minecraft.getInstance().level, EntitySpawnReason.MOB_SUMMONED)\n                ).toList();\n            }\n\n            private void animate(Duration delta) {\n                var seconds = delta.toNanos() / (double) Duration.ofSeconds(1).toNanos();\n                this.pitch += this.pitchDelta * seconds * 10;\n                this.yaw += this.yawDelta * seconds * 10;\n\n                this.pitchDelta += Delta.compute(this.pitchDelta, 0, seconds * .5);\n                this.yawDelta += Delta.compute(this.yawDelta, 0, seconds * .5);\n\n                if (Math.abs(this.pitchDelta) > 1e-4 || Math.abs(this.yawDelta) > 1e-4) {\n                    this.scheduleAnimationCallback(this::animate);\n                }\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Row(\n                    MainAxisAlignment.CENTER,\n                    CrossAxisAlignment.CENTER,\n                    new Padding(Insets.all(10)),\n                    List.of(\n                        new VerticalCarouselThing(\n                            this.selectedEntityIdx,\n                            idx -> this.setState(() -> this.selectedEntityIdx = idx),\n                            Size.square(64), this.entities.stream().<Widget>map(entity -> new EntityWidget(.75, entity, null)).toList()),\n                        new MouseArea(\n                            widget -> widget\n                                .clickCallback((x, y, button, modifiers) -> {\n                                    this.yawDelta = 0;\n                                    this.pitchDelta = 0;\n\n                                    return true;\n                                })\n                                .dragCallback((x, y, dx, dy) -> {\n                                    this.yaw += dx * .5;\n                                    this.pitch += dy * .5;\n\n                                    this.yawDelta = dx;\n                                    this.pitchDelta = dy;\n\n                                    this.releasedLate = false;\n                                    this.scheduleDelayedCallback(Duration.ofMillis(200), () -> {\n                                        this.releasedLate = true;\n                                    });\n                                })\n                                .dragEndCallback(() -> {\n                                    if (this.releasedLate) return;\n                                    this.animate(Duration.ZERO);\n                                }),\n                            new Sized(\n                                Size.square(400),\n                                new EntityWidget(\n                                    .75,\n                                    this.entities.get(this.selectedEntityIdx),\n                                    widget -> widget\n                                        .displayMode(EntityWidget.DisplayMode.NONE)\n                                        .transform(matrix4f -> {\n                                            matrix4f.rotateX((float) Math.toRadians(this.pitch));\n                                            matrix4f.rotateY((float) Math.toRadians(this.yaw));\n                                        })\n                                )\n                            )\n                        )\n                    )\n                );\n            }\n        }\n\n        public static class VerticalCarouselThing extends StatefulWidget {\n\n            public final int selectedIndex;\n            public final IntConsumer onChanged;\n            public final Size itemSize;\n            public final List<Widget> children;\n\n            public VerticalCarouselThing(int selectedIndex, IntConsumer onChanged, Size itemSize, List<Widget> children) {\n                this.selectedIndex = selectedIndex;\n                this.onChanged = onChanged;\n                this.itemSize = itemSize;\n                this.children = children;\n            }\n\n            @Override\n            public WidgetState<VerticalCarouselThing> createState() {\n                return new State();\n            }\n\n            public static class State extends WidgetState<VerticalCarouselThing> {\n\n                @Override\n                public Widget build(BuildContext context) {\n                    var displayChildren = new ArrayList<Widget>();\n                    displayChildren.add(new Panel(Panel.VANILLA_INSET));\n\n                    var offset = -this.widget().selectedIndex * this.widget().itemSize.height() / 2;\n                    offset -= this.widget().itemSize.height() / 4;\n\n                    for (var i = 0; i < this.widget().children.size(); i++) {\n                        var thisOffset = (float) offset;\n\n                        var scale = i == this.widget().selectedIndex ? 1 : .5f;\n                        offset += this.widget().itemSize.height() * scale;\n\n                        if (scale == 1) {\n                            //noinspection lossy-conversions\n                            thisOffset += this.widget().itemSize.height() / 4f;\n                        }\n\n                        var elementIndex = i;\n                        displayChildren.add(\n                            new Transform(\n                                new Matrix3x2f()\n                                    .translate(0f, thisOffset)\n                                    .scale(scale, scale),\n                                Interactable.primary(\n                                    () -> this.widget().onChanged.accept(elementIndex),\n                                    this.widget().children.get(i)\n                                )\n                            ));\n                    }\n\n                    return new MouseArea(\n                        widget -> widget\n                            .scrollCallback((horizontal, vertical) -> {\n                                if (vertical == 0) return false;\n                                this.setState(() -> {\n                                    this.widget().onChanged.accept(Mth.clamp(\n                                        this.widget().selectedIndex - (int) Math.signum(vertical),\n                                        0,\n                                        this.widget().children.size() - 1\n                                    ));\n                                });\n\n                                return true;\n                            }),\n                        new Row(\n                            MainAxisAlignment.CENTER,\n                            CrossAxisAlignment.CENTER,\n                            new Sized(\n                                Size.of(18, this.widget().itemSize.height()),\n                                new Grid(\n                                    LayoutAxis.HORIZONTAL,\n                                    2,\n                                    Grid.CellFit.tight(),\n                                    new MessageButton(Component.literal(\"↑\"), this.widget().selectedIndex > 0 ? () -> this.widget().onChanged.accept(this.widget().selectedIndex - 1) : null),\n                                    new MessageButton(Component.literal(\"↓\"), this.widget().selectedIndex < this.widget().children.size() - 1 ? () -> this.widget().onChanged.accept(this.widget().selectedIndex + 1) : null)\n                                )\n                            ),\n                            new Sized(\n                                this.widget().itemSize,\n                                new Stack(\n                                    displayChildren\n                                )\n                            )\n                        )\n                    );\n                }\n            }\n        }\n    }\n\n    public static class OptimizationTest extends StatelessWidget {\n\n        @Override\n        public Widget build(BuildContext context) {\n            var widget = new Sized(\n                128, 128,\n                new EntityWidget(\n                    1.5, BurningChyz.of(context),\n                    entityWidget -> entityWidget.displayMode(EntityWidget.DisplayMode.CURSOR)\n                )\n            );\n            return new VerticallyScrollable(\n                new Grid(\n                    LayoutAxis.VERTICAL,\n                    32,\n                    Grid.CellFit.loose(),\n                    Stream.generate(() -> widget).limit(32 * 32).toList()\n                )\n            );\n        }\n    }\n\n    public static class AutomaticAnimationTest extends StatefulWidget {\n        @Override\n        public WidgetState<AutomaticAnimationTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<AutomaticAnimationTest> {\n\n            private double x = 0;\n            private double y = 0;\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new MouseArea(\n                    widget -> widget\n                        .clickCallback((toX, toY, button, mods) -> {\n                            this.setState(() -> {\n                                this.x = toX;\n                                this.y = toY;\n                            });\n\n                            return true;\n                        }).dragCallback((x, y, dx, dy) -> this.setState(() -> {\n                            this.x = x;\n                            this.y = y;\n                        })),\n                    new LayoutBuilder((context1, constraints) -> {\n                        System.out.println(\"layout rebuild\");\n                        return new Constrain(\n                            Constraints.of(constraints.maxWidth(), constraints.maxHeight(), constraints.maxWidth(), constraints.maxHeight()),\n                            new DragArena(\n                                new TheWidget(\n                                    Duration.ofMillis(250),\n                                    Easing.OUT_EXPO,\n                                    this.x,\n                                    this.y\n                                )\n                            )\n                        );\n                    })\n                );\n            }\n        }\n\n        public static class TheWidget extends AutomaticallyAnimatedWidget {\n\n            public final double x;\n            public final double y;\n\n            public TheWidget(Duration duration, Easing easing, double x, double y) {\n                super(duration, easing);\n                this.x = x;\n                this.y = y;\n            }\n\n            @Override\n            public State createState() {\n                return new State();\n            }\n\n            public static class State extends AutomaticallyAnimatedWidget.State<TheWidget> {\n\n                private DoubleLerp x;\n                private DoubleLerp y;\n\n                @Override\n                protected void updateLerps() {\n                    this.x = this.visitLerp(this.x, this.widget().x, DoubleLerp::new);\n                    this.y = this.visitLerp(this.y, this.widget().y, DoubleLerp::new);\n                }\n\n                @Override\n                public Widget build(BuildContext context) {\n                    return new DragArenaElement(\n                        this.x.compute(this.animationValue()),\n                        this.y.compute(this.animationValue()),\n                        new Align(\n                            Alignment.of(-.5, -.5),\n                            2d, 2d,\n                            new BraidLogo()\n                        )\n                    );\n                }\n            }\n        }\n    }\n\n    public static class KdlWidgetsTest extends StatefulWidget {\n        @Override\n        public WidgetState<KdlWidgetsTest> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<KdlWidgetsTest> {\n\n            private TextEditingController textController;\n\n            private KdlNode rootNode;\n            private Widget kdlWidget = EmptyWidget.INSTANCE;\n\n            @Override\n            public void init() {\n                this.textController = new TextEditingController();\n                this.textController.addListener(() -> {\n                    try {\n                        var parsedKdl = new Kdl2Parser().parse(this.textController.value().text());\n                        this.rootNode = parsedKdl.nodes().getFirst();\n\n                        var deserializer = new KdlDeserializer(this.rootNode, KdlMapper.DEFAULT_MAPPERS);\n                        var ctx = deserializer.setupContext(SerializationContext.attributes(\n                            SerializationAttributes.HUMAN_READABLE,\n                            BraidKdlEndecs.HANDLERS.instance(Map.of(\"lmao\", (theArg) -> System.out.println(\"lmao: \" + theArg)))\n                        ));\n\n                        var parsedWidget = WidgetEndec.ROOT.decode(ctx, deserializer);\n                        this.setState(() -> {\n                            this.kdlWidget = parsedWidget;\n                        });\n                    } catch (Exception e) {\n                        Throwable cause = e;\n                        while (cause.getCause() != null) {\n                            cause = cause.getCause();\n                        }\n\n                        var bruhJava = cause;\n                        this.setState(() -> {\n                            this.kdlWidget = new Label(LabelStyle.SHADOW, true, Component.literal(Objects.requireNonNullElse(bruhJava.getMessage(), \"no message\")).withStyle(ChatFormatting.RED));\n                        });\n                    }\n                });\n            }\n\n            private void showJson() {\n                var deserializer = new KdlDeserializer(this.rootNode, KdlMapper.DEFAULT_MAPPERS);\n                var jsonOut = GsonSerializer.of();\n\n                deserializer.readAny(SerializationContext.attributes(SerializationAttributes.HUMAN_READABLE), jsonOut);\n                var jsonText = new GsonBuilder().setPrettyPrinting().create().toJson(jsonOut.result());\n\n                var widget = new Box(\n                    Color.mix(.1, Color.BLACK, Color.WHITE),\n                    new VerticallyScrollable(\n                        null,\n                        ScrollAnimationSettings.DEFAULT,\n                        new Padding(\n                            Insets.all(5),\n                            new Label(\n                                new LabelStyle(Alignment.TOP_LEFT, null, Style.EMPTY.withFont(new FontDescription.Resource(Minecraft.UNIFORM_FONT)), false),\n                                true,\n                                Component.literal(jsonText)\n                            )\n                        )\n                    )\n                );\n\n                BraidWindow.open(\n                    \"json preview\",\n                    650, 650,\n                    widget\n                );\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new Row(\n                    MainAxisAlignment.START,\n                    CrossAxisAlignment.CENTER,\n                    new Column(\n                        MainAxisAlignment.START,\n                        CrossAxisAlignment.END,\n                        new Sized(\n                            350, 300,\n                            new TextBox(\n                                this.textController,\n                                widget -> widget\n                                    .placeholder(Component.literal(\"KDL goes here\"))\n                                    .softWrap(false)\n                            )\n                        ),\n                        new MessageButton(\n                            Component.literal(\"view as json\"),\n                            this::showJson\n                        )\n                    ),\n                    new Sized(\n                        Size.square(250),\n                        this.kdlWidget\n                    )\n                );\n            }\n        }\n    }\n\n//    public static class BeegGrid extends StatefulWidget {\n//        @Override\n//        public WidgetState<BeegGrid> createState() {\n//            return new State();\n//        }\n//\n//        public static class State extends WidgetState<BeegGrid> {\n//\n//            private List<String> lines;\n//\n//            @Override\n//            public void init() {\n//                try {\n//                    this.lines = Files.readAllLines(Path.of(\"sounds.json\"));\n//                } catch (IOException e) {\n//                    throw new RuntimeException(e);\n//                }\n//            }\n//\n//            @Override\n//            public Widget build(BuildContext context) {\n//                var children = new ArrayList<Widget>();\n//\n//                for (var lineIdx = 0; lineIdx < this.lines.size(); lineIdx++) {\n//                    var line = this.lines.get(lineIdx);\n//\n//                    children.add(new Label(Text.literal(String.valueOf(lineIdx))));\n//                    children.add(new Label(Text.literal(line)));\n//                }\n//\n//                return new VerticallyScrollable(\n//                    new Grid(\n//                        LayoutAxis.VERTICAL,\n//                        2,\n//                        Grid.CellFit.loose(Alignment.LEFT),\n//                        children\n//                    )\n//                );\n//            }\n//        }\n//    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/config/UowouConfigModel.java",
    "content": "package io.wispforest.uwu.config;\n\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.config.annotation.*;\nimport io.wispforest.owo.ui.core.Color;\nimport net.minecraft.resources.Identifier;\n\nimport java.util.Set;\n\n@Modmenu(modId = \"fabric\")\n@Config(name = \"uowou\", wrapperName = \"BruhConfig\")\n@Sync(Option.SyncMode.OVERRIDE_CLIENT)\npublic class UowouConfigModel {\n\n    @RestartRequired\n    public boolean thisIsNotSyncable = false;\n\n    @Hook\n    public Identifier idPlease = Identifier.fromNamespaceAndPath(\"uowou\", \"bruh\");\n\n    @Sync(Option.SyncMode.NONE)\n    public Set<String> setPlease = Set.of(\"that's a value\");\n\n    public Color bruhve = Color.BLACK;\n}\n\n\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/config/UwuConfigModel.java",
    "content": "package io.wispforest.uwu.config;\n\nimport blue.endless.jankson.Comment;\nimport io.wispforest.owo.config.Option;\nimport io.wispforest.owo.config.annotation.*;\nimport io.wispforest.owo.ui.core.Color;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Sync(Option.SyncMode.OVERRIDE_CLIENT)\n@Modmenu(modId = \"uwu\", uiModelId = \"uwu:config\")\n@Config(name = \"uwu\", wrapperName = \"UwuConfig\")\npublic class UwuConfigModel {\n\n    @SectionHeader(\"top\")\n    @RangeConstraint(min = 0, max = 56)\n    public int aValue = 56;\n\n    @RegexConstraint(\"[A-Za-z]{1,3}\")\n    public String regex = \"yes\";\n\n    @Nest\n    @Expanded\n    @SectionHeader(\"nesting_yo?\")\n    public Nested nestingTime = new Nested();\n\n    @PredicateConstraint(\"predicateFunction\")\n    public List<String> someOption = new ArrayList<>(List.of(\"1\", \"2\", \"3\", \"4\", \"5\"));\n\n    @RangeConstraint(min = 0, max = 10, decimalPlaces = 1)\n    public float floting = 6.9f;\n\n    public String thisIsAStringValue = \"\\\\bruh?\";\n\n    @SectionHeader(\"bottom\")\n    public List<String> thereAreStringsHere = new ArrayList<>(List.of(\"yes\", \"no\"));\n\n    @RestartRequired\n    public WowValues broTheresAnEnum = WowValues.FIRST;\n\n    public Color anEpicColor = Color.BLUE;\n\n    @WithAlpha\n    public Color anEpicColorWithAlpha = Color.GREEN;\n\n    @ExcludeFromScreen\n    public String noSeeingThis = \"yep, never\";\n\n    public static class Nested {\n        public boolean togglee = false;\n        public boolean yesThisIsAlsoNested = true;\n\n        @Nest\n        @Comment(\"Commented nesting\")\n        public SuperNested nestingTimeIntensifies = new SuperNested();\n\n        @Sync(Option.SyncMode.INFORM_SERVER)\n        public List<Integer> nestedIntegers = new ArrayList<>(List.of(69, 34, 35, 420));\n    }\n\n    public static class SuperNested {\n        public byte wowSoNested;\n    }\n\n    public enum WowValues {\n        FIRST, SECOND, THIRD, FOURTH;\n    }\n\n    // so we declare a predicate method\n    public static boolean predicateFunction(List<String> list) {\n        // and do the check in here\n        // this could be arbitrarily complex code, but\n        // we'll keep it simple for this demonstration\n        return list.size() == 5;\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/items/UwuBraidItem.java",
    "content": "package io.wispforest.uwu.items;\n\nimport io.wispforest.owo.braid.core.*;\nimport io.wispforest.owo.braid.display.BraidDisplay;\nimport io.wispforest.owo.braid.display.BraidDisplayBinding;\nimport io.wispforest.owo.braid.display.DisplayQuad;\nimport io.wispforest.owo.braid.framework.BuildContext;\nimport io.wispforest.owo.braid.framework.proxy.WidgetState;\nimport io.wispforest.owo.braid.framework.widget.StatefulWidget;\nimport io.wispforest.owo.braid.framework.widget.StatelessWidget;\nimport io.wispforest.owo.braid.framework.widget.Widget;\nimport io.wispforest.owo.braid.widgets.object.EntityWidget;\nimport io.wispforest.owo.braid.widgets.object.ItemStackWidget;\nimport io.wispforest.owo.braid.widgets.Navigator;\nimport io.wispforest.owo.braid.widgets.basic.*;\nimport io.wispforest.owo.braid.widgets.button.Button;\nimport io.wispforest.owo.braid.widgets.button.MessageButton;\nimport io.wispforest.owo.braid.widgets.flex.*;\nimport io.wispforest.owo.braid.widgets.label.Label;\nimport io.wispforest.owo.braid.widgets.label.LabelStyle;\nimport io.wispforest.owo.braid.widgets.stack.Stack;\nimport io.wispforest.owo.braid.widgets.stack.StackBase;\nimport io.wispforest.uwu.client.braid.TestSelector;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.world.entity.Entity;\nimport net.minecraft.world.entity.EntityType;\nimport net.minecraft.world.entity.animal.cow.Cow;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.inventory.tooltip.TooltipComponent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.InteractionResult;\nimport net.minecraft.util.CommonColors;\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.world.item.Rarity;\nimport net.minecraft.world.phys.Vec3;\nimport net.minecraft.world.level.Level;\n\nimport java.util.List;\nimport java.util.Optional;\n\npublic class UwuBraidItem extends Item {\n\n    @Environment(EnvType.CLIENT)\n    private static BraidDisplay display;\n\n    public UwuBraidItem(Properties settings) {\n        super(settings.rarity(Rarity.EPIC));\n    }\n\n    @Override\n    @Environment(EnvType.CLIENT)\n    public InteractionResult use(Level world, Player user, InteractionHand hand) {\n        if (world.isClientSide()) {\n            if (user.isShiftKeyDown()) {\n                if (display == null) {\n                    display = new BraidDisplay(\n                        new DisplayQuad(Vec3.ZERO, new Vec3(5, 0, 0), new Vec3(0, -3, 0)),\n                        750,\n                        450,\n                        new DisplayApp()\n                    ).renderAutomatically();\n\n                    BraidDisplayBinding.activate(display);\n                }\n\n                var right = new Vec3(-5, 0, 0)\n                    .xRot((float) Math.toRadians(-user.getXRot()))\n                    .yRot((float) Math.toRadians(-user.getYRot()));\n\n                var down = new Vec3(0, -3, 0)\n                    .xRot((float) Math.toRadians(-user.getXRot()))\n                    .yRot((float) Math.toRadians(-user.getYRot()));\n\n                display.quad = new DisplayQuad(\n                    user.getEyePosition()\n                        .add(user.getForward().scale(2.5))\n                        .subtract(right.scale(.5))\n                        .subtract(down.scale(.5)),\n                    right, down\n                );\n            } else {\n                openTestSelector();\n            }\n        }\n        return InteractionResult.PASS;\n    }\n\n    @Override\n    public Optional<TooltipComponent> getTooltipImage(ItemStack stack) {\n        return Optional.of(new Tooltip());\n    }\n\n    public static void openTestSelector() {\n        var settings = new BraidScreen.Settings();\n        settings.shouldPause = false;\n\n        Minecraft.getInstance().setScreen(new BraidScreen(settings, new TestSelector()));\n    }\n\n    public record Tooltip() implements TooltipComponent {}\n\n    public static class DisplayApp extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            return new Stack(\n                new StackBase(new Navigator(new DisplayAppRoute())),\n                new Padding(\n                    Insets.all(1),\n                    new HoverableBuilder(\n                        (hoverableContext, hovered) -> new Box(\n                            hovered ? Color.rgb(CommonColors.BLUE) : Color.WHITE,\n                            true\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    public static class DisplayAppRoute extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            return new Center(\n                new Stack(\n                    new Column(\n                        MainAxisAlignment.START,\n                        CrossAxisAlignment.CENTER,\n                        new Padding(Insets.vertical(4)),\n                        List.of(\n                            new Sized(\n                                Size.square(96),\n                                new Cow()\n                            ),\n                            new Panel(\n                                Panel.VANILLA_DARK,\n                                new Padding(\n                                    Insets.all(10),\n                                    new Label(Component.translatable(\"text.uwu.braid\").append(Component.literal(\" in world real??\")))\n                                )\n                            ),\n                            new IntrinsicHeight(\n                                new Row(\n                                    MainAxisAlignment.START,\n                                    CrossAxisAlignment.CENTER,\n                                    new Button(\n                                        UwuBraidItem::openTestSelector,\n                                        new Sized(\n                                            16,\n                                            16,\n                                            new ItemStackWidget(UwuItems.BRAID.getDefaultInstance())\n                                        )\n                                    ),\n                                    new Button(\n                                        () -> Minecraft.getInstance().player.handleCreativeModeItemDrop(UwuItems.BRAID.getDefaultInstance()),\n                                        new Label(\n                                            LabelStyle.SHADOW,\n                                            true,\n                                            Component.translatable(\"text.uwu.braid\").append(Component.literal(\" button\"))\n                                        )\n                                    )\n                                )\n                            ),\n                            new MessageButton(\n                                Component.literal(\"test selector\"),\n                                () -> {\n                                    Navigator.push(context, new TestSelectorRoute());\n                                }\n                            )\n                        )\n                    ),\n                    new Align(\n                        Alignment.TOP_RIGHT,\n                        new Padding(\n                            Insets.all(4),\n                            new MessageButton(\n                                Component.literal(\"x\"),\n                                () -> Minecraft.getInstance().schedule(() -> {\n                                    BraidDisplayBinding.deactivate(display);\n\n                                    display.app.dispose();\n                                    display = null;\n                                })\n                            )\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    public static class TestSelectorRoute extends StatelessWidget {\n        @Override\n        public Widget build(BuildContext context) {\n            return new Column(\n                MainAxisAlignment.START,\n                CrossAxisAlignment.CENTER,\n                new Flexible(\n                    new TestSelector()\n                ),\n                new Align(\n                    Alignment.BOTTOM,\n                    new Row(\n                        new MessageButton(\n                            Component.literal(\"back\"),\n                            () -> Navigator.pop(context)\n                        ),\n                        new MessageButton(\n                            Component.literal(\"inspector\"),\n                            () -> AppState.of(context).activateInspector()\n                        )\n                    )\n                )\n            );\n        }\n    }\n\n    public static class Cow extends StatefulWidget {\n        @Override\n        public WidgetState<Cow> createState() {\n            return new State();\n        }\n\n        public static class State extends WidgetState<Cow> {\n\n            private Entity cow;\n\n            @Override\n            public void init() {\n                this.cow = new net.minecraft.world.entity.animal.cow.Cow(EntityType.COW, Minecraft.getInstance().level);\n            }\n\n            @Override\n            public Widget build(BuildContext context) {\n                return new EntityWidget(\n                    1.35,\n                    this.cow,\n                    widget -> widget.displayMode(EntityWidget.DisplayMode.CURSOR)\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/items/UwuCounterItem.java",
    "content": "package io.wispforest.uwu.items;\n\nimport io.wispforest.endec.Endec;\nimport net.minecraft.core.component.DataComponentPatch;\nimport net.minecraft.core.component.DataComponentMap;\nimport net.minecraft.core.component.DataComponentType;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.world.entity.EquipmentSlotGroup;\nimport net.minecraft.world.item.component.ItemAttributeModifiers;\nimport net.minecraft.world.entity.ai.attributes.AttributeModifier;\nimport net.minecraft.world.entity.ai.attributes.Attributes;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.Registry;\nimport net.minecraft.world.InteractionResult;\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.item.Rarity;\nimport net.minecraft.world.level.Level;\n\npublic class UwuCounterItem extends Item {\n    private static final DataComponentType<Integer> COUNT = Registry.register(\n        BuiltInRegistries.DATA_COMPONENT_TYPE,\n        Identifier.fromNamespaceAndPath(\"uwu\", \"count\"),\n        DataComponentType.<Integer>builder()\n            .endec(Endec.INT)\n            .build()\n    );\n\n    public UwuCounterItem(Item.Properties settings) {\n        super(settings.rarity(Rarity.UNCOMMON));\n    }\n\n    @Override\n    public InteractionResult use(Level world, Player user, InteractionHand hand) {\n        var stack = user.getItemInHand(hand);\n\n        if (user.isShiftKeyDown()) {\n            stack.update(COUNT, 0, old -> old - 1);\n        } else {\n            stack.update(COUNT, 0, old -> old + 1);\n        }\n\n        return InteractionResult.SUCCESS.heldItemTransformedTo(stack);\n    }\n\n    @Override\n    public void deriveStackComponents(DataComponentMap source, DataComponentPatch.Builder target) {\n        target.set(DataComponents.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.builder()\n            .add(Attributes.ATTACK_DAMAGE,\n                new AttributeModifier(Identifier.fromNamespaceAndPath(\"uwu\", \"counter_attribute\"), source.getOrDefault(COUNT, 0), AttributeModifier.Operation.ADD_VALUE),\n                EquipmentSlotGroup.MAINHAND)\n            .build());\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/items/UwuItems.java",
    "content": "package io.wispforest.uwu.items;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.samples.braid.BraidSamplesItem;\nimport io.wispforest.uwu.Uwu;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.Registry;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.Identifier;\n\nimport java.util.function.Function;\n\npublic class UwuItems {\n\n    public static final Item TEST_STICK = register(\"test_stick\", UwuTestStickItem::new);\n    public static final Item SCREEN_SHARD = register(\"screen_shard\", UwuScreenShardItem::new);\n    public static final Item COUNTER = register(\"counter\", UwuCounterItem::new);\n    public static final Item BRAID = register(\"braid\", UwuBraidItem::new);\n    public static final Item BRAID_SAMPLES = register(\"braid_samples\", BraidSamplesItem::new);\n\n    public static final Item OWO_INGOT = register(Identifier.fromNamespaceAndPath(\"uowou\", \"owo_ingot\"), new Item.Properties().group(Uwu.FOUR_TAB_GROUP).tab(2).stacksTo(69));\n\n\n    public static <T extends Item> T register(String path, Function<Item.Properties, T> factory) {\n        return register(path, factory, new Item.Properties());\n    }\n\n    public static Item register(String path, Item.Properties settings) {\n        return register(Identifier.fromNamespaceAndPath(\"uwu\", path), Item::new, settings);\n    }\n\n    public static <T extends Item> T register(String path, Function<Item.Properties, T> factory, Item.Properties settings) {\n        return register(Identifier.fromNamespaceAndPath(\"uwu\", path), factory, settings);\n    }\n\n    public static Item register(Identifier identifier, Item.Properties settings) {\n        return register(identifier, Item::new, settings);\n    }\n\n\n    public static <T extends Item> T register(Identifier identifier, Function<Item.Properties, T> factory, Item.Properties settings) {\n        var registryKey = ResourceKey.create(Registries.ITEM, identifier);\n\n        settings.setId(registryKey);\n\n        T t = factory.apply(settings);\n\n        return Registry.register(BuiltInRegistries.ITEM, registryKey, t);\n    }\n\n    public static void init() {}\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/items/UwuScreenShardItem.java",
    "content": "package io.wispforest.uwu.items;\n\nimport io.wispforest.uwu.EpicMenu;\nimport io.wispforest.uwu.client.SelectUwuScreenScreen;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.entity.player.Inventory;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.MenuProvider;\nimport net.minecraft.world.inventory.AbstractContainerMenu;\nimport net.minecraft.world.inventory.ContainerLevelAccess;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.InteractionResult;\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.world.item.Rarity;\nimport net.minecraft.world.level.Level;\nimport org.jetbrains.annotations.NotNull;\n\npublic class UwuScreenShardItem extends Item {\n\n    public UwuScreenShardItem(Item.Properties settings) {\n        super(settings.rarity(Rarity.UNCOMMON));\n    }\n\n    @Override\n    @Environment(EnvType.CLIENT)\n    public InteractionResult use(Level world, Player user, InteractionHand hand) {\n        if (user.isShiftKeyDown()) {\n            if (world.isClientSide()) Minecraft.getInstance().setScreen(new SelectUwuScreenScreen());\n        } else if (!world.isClientSide()) {\n            user.openMenu(new MenuProvider() {\n                @Override\n                public Component getDisplayName() {\n                    return Component.literal(\"bruh momento\");\n                }\n\n                @Override\n                public @NotNull AbstractContainerMenu createMenu(int syncId, Inventory inv, Player player) {\n                    return new EpicMenu(syncId, inv, ContainerLevelAccess.create(world, player.blockPosition()));\n                }\n            });\n        }\n        return InteractionResult.PASS;\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/items/UwuTestStickItem.java",
    "content": "package io.wispforest.uwu.items;\n\nimport com.mojang.datafixers.util.Pair;\nimport com.mojang.serialization.Codec;\nimport com.mojang.serialization.DataResult;\nimport com.mojang.serialization.DynamicOps;\nimport io.wispforest.endec.impl.KeyedEndec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.itemgroup.OwoItemGroup;\nimport io.wispforest.owo.ops.LevelOps;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.SerializationContext;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport io.wispforest.owo.serialization.RegistriesAttribute;\nimport io.wispforest.owo.serialization.endec.MinecraftEndecs;\nimport io.wispforest.uwu.Uwu;\nimport io.wispforest.uwu.text.BasedTextContent;\nimport net.minecraft.core.component.DataComponentType;\nimport net.minecraft.core.component.DataComponents;\nimport net.minecraft.world.item.component.CustomData;\nimport net.minecraft.world.item.enchantment.Enchantments;\nimport net.minecraft.world.entity.player.Player;\nimport net.minecraft.world.item.Item;\nimport net.minecraft.world.item.ItemStack;\nimport net.minecraft.world.item.context.UseOnContext;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.core.registries.BuiltInRegistries;\nimport net.minecraft.core.Registry;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.resources.RegistryOps;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.network.chat.MutableComponent;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.world.InteractionResult;\nimport net.minecraft.world.InteractionHand;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.world.phys.Vec3;\nimport net.minecraft.world.level.Level;\n\npublic class UwuTestStickItem extends Item {\n\n    private static final DataComponentType<Component> TEXT_COMPONENT = Registry.register(\n            BuiltInRegistries.DATA_COMPONENT_TYPE,\n            Identifier.fromNamespaceAndPath(\"uwu\", \"text\"),\n            DataComponentType.<Component>builder()\n                    .endec(MinecraftEndecs.TEXT)\n                    .build()\n    );\n\n    private static final Codec<String> THIS_CODEC_NEEDS_REGISTRIES = new Codec<>() {\n        @Override\n        public <T> DataResult<Pair<String, T>> decode(DynamicOps<T> ops, T input) {\n            if (!(ops instanceof RegistryOps<T>)) return DataResult.error(() -> \"need the registries bro\");\n            return DataResult.success(new Pair<>(ops.getStringValue(input).getOrThrow(), input));\n        }\n        @Override\n        public <T> DataResult<T> encode(String input, DynamicOps<T> ops, T prefix) {\n            if (!(ops instanceof RegistryOps<T>)) return DataResult.error(() -> \"need the registries bro\");\n            return DataResult.success(ops.createString(input));\n        }\n    };\n\n    private static final Endec<String> YEP_SAME_HERE = CodecUtils.toEndec(CodecUtils.toCodec(CodecUtils.toEndec(THIS_CODEC_NEEDS_REGISTRIES)));\n    private static final KeyedEndec<String> KYED = YEP_SAME_HERE.keyed(\"kyed\", (String) null);\n\n    public UwuTestStickItem(Item.Properties settings) {\n        super(settings\n                .group(Uwu.SIX_TAB_GROUP).tab(3).stacksTo(1)\n                .trackUsageStat()\n                .stackGenerator(OwoItemGroup.DEFAULT_STACK_GENERATOR.andThen((item, stacks) -> {\n                    final var stack = new ItemStack(item);\n                    stack.set(DataComponents.CUSTOM_NAME, Component.literal(\"the stick of the test\").withStyle(style -> style.withItalic(false)));\n                    stacks.accept(stack);\n                })));\n\n        Uwu.CHANNEL.registerServerbound(ThatPacket.class, StructEndecBuilder.of(YEP_SAME_HERE.fieldOf(\"mhmm\", ThatPacket::mhmm), ThatPacket::new), (message, access) -> {\n            System.out.println(\"that's a packet received alright: \" + message.mhmm);\n        });\n    }\n\n    @Override\n    public InteractionResult use(Level world, Player user, InteractionHand hand) {\n        if (user.isShiftKeyDown()) {\n            if (world.isClientSide()) return InteractionResult.SUCCESS;\n\n            Uwu.CHANNEL.serverHandle(user).send(new Uwu.OtherTestMessage(user.blockPosition(), \"based\"));\n\n            var server = user.level().getServer();\n            var teleportTo = world.dimension() == Level.END ? server.getLevel(Level.OVERWORLD) : server.getLevel(Level.END);\n\n            LevelOps.teleportToLevel((ServerPlayer) user, teleportTo, new Vec3(0, 128, 0));\n\n        } else {\n            if (!world.isClientSide()) return InteractionResult.SUCCESS;\n\n            Uwu.CHANNEL.clientHandle().send(Uwu.MESSAGE);\n\n            Uwu.CUBE.spawn(world, user.getEyePosition().add(user.getViewVector(0).scale(3)).subtract(.5, .5, .5), null);\n            user.displayClientMessage(Component.translatable(\"uwu.a\", \"bruh\"), false);\n        }\n\n        return InteractionResult.SUCCESS;\n    }\n\n    @Override\n    public InteractionResult useOn(UseOnContext context) {\n        if (!context.getPlayer().isShiftKeyDown()) {\n            if (context.getLevel().isClientSide()) Uwu.CHANNEL.clientHandle().send(new ThatPacket(\"stringnite\"));\n\n            try {\n                var stack = context.getItemInHand();\n                var data = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag()\n                        .get(SerializationContext.attributes(RegistriesAttribute.of(context.getLevel().registryAccess())), KYED);\n\n                context.getPlayer().displayClientMessage(Component.literal(\"current: \" + data), false);\n\n                stack.update(DataComponents.CUSTOM_DATA, CustomData.EMPTY, nbt -> {\n                    return nbt.update(nbtCompound -> nbtCompound.put(\n                            SerializationContext.attributes(RegistriesAttribute.of(context.getLevel().registryAccess())),\n                            KYED,\n                            String.valueOf(context.getLevel().random.nextInt(10000))\n                    ));\n                });\n                context.getPlayer().displayClientMessage(Component.literal(\"modified\"), false);\n            } catch (Exception bruh) {\n                context.getPlayer().displayClientMessage(Component.literal(\"bruh: \" + bruh.getMessage()), false);\n            }\n\n            return InteractionResult.SUCCESS;\n        }\n\n        if (context.getLevel().isClientSide()) return InteractionResult.SUCCESS;\n\n        final var breakStack = new ItemStack(Items.NETHERITE_PICKAXE);\n\n        final var fortune = context.getLevel().registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.FORTUNE);\n        breakStack.enchant(fortune, 3);\n        LevelOps.breakBlockWithItem(context.getLevel(), context.getClickedPos(), breakStack);\n\n        final var stickStack = context.getItemInHand();\n\n        if (!stickStack.has(TEXT_COMPONENT)) {\n            stickStack.set(TEXT_COMPONENT, Component.nullToEmpty(String.valueOf(context.getLevel().random.nextInt(1000000))));\n        }\n\n        stickStack.set(TEXT_COMPONENT, MutableComponent.create(new BasedTextContent(\"basednite, \")).append(stickStack.get(TEXT_COMPONENT)));\n\n        context.getPlayer().displayClientMessage(stickStack.get(TEXT_COMPONENT), false);\n\n        Uwu.BREAK_BLOCK_PARTICLES.spawn(context.getLevel(), Vec3.atLowerCornerOf(context.getClickedPos()), null);\n\n        return InteractionResult.SUCCESS;\n    }\n\n    private record ThatPacket(String mhmm) {}\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/mixin/GlRenderPassMixin.java",
    "content": "package io.wispforest.uwu.mixin;\n\nimport com.llamalad7.mixinextras.injector.wrapoperation.Operation;\nimport com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;\nimport io.wispforest.owo.Owo;\nimport com.mojang.blaze3d.opengl.GlRenderPass;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\n\n@Mixin(GlRenderPass.class)\npublic class GlRenderPassMixin {\n    @WrapOperation(method = \"<clinit>\", at = @At(value = \"FIELD\", target = \"Lnet/minecraft/SharedConstants;IS_RUNNING_IN_IDE:Z\"))\n    private static boolean adjustDevCheck(Operation<Boolean> original) {\n        return original.call() || Owo.DEBUG;\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/mixin/TitleScreenMixin.java",
    "content": "package io.wispforest.uwu.mixin;\n\nimport io.wispforest.owo.config.ui.ConfigScreen;\nimport io.wispforest.uwu.Uwu;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.client.gui.screens.TitleScreen;\nimport net.minecraft.client.gui.components.Button;\nimport com.mojang.blaze3d.platform.InputConstants;\nimport net.minecraft.network.chat.Component;\nimport org.lwjgl.glfw.GLFW;\nimport org.spongepowered.asm.mixin.Mixin;\nimport org.spongepowered.asm.mixin.injection.At;\nimport org.spongepowered.asm.mixin.injection.Inject;\nimport org.spongepowered.asm.mixin.injection.callback.CallbackInfo;\n\n@Mixin(TitleScreen.class)\npublic abstract class TitleScreenMixin extends Screen {\n\n    protected TitleScreenMixin(Component title) {\n        super(title);\n    }\n\n    @Inject(method = \"method_41198\", at = @At(\"HEAD\"), cancellable = true)\n    private void injectUwuConfigScreen(Button button, CallbackInfo ci) {\n        var alt = InputConstants.isKeyDown(Minecraft.getInstance().getWindow(), GLFW.GLFW_KEY_LEFT_ALT)\n            || InputConstants.isKeyDown(Minecraft.getInstance().getWindow(), GLFW.GLFW_KEY_RIGHT_ALT);\n        if (!alt) return;\n\n        Minecraft.getInstance().setScreen(ConfigScreen.create(Uwu.BRUHHHHH, this));\n        ci.cancel();\n    }\n\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/DispatchedInterface.java",
    "content": "package io.wispforest.uwu.network;\n\npublic interface DispatchedInterface {\n    String getName();\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/DispatchedSubclassOne.java",
    "content": "package io.wispforest.uwu.network;\n\npublic record DispatchedSubclassOne(String a) implements DispatchedInterface {\n    @Override\n    public String getName() {\n        return \"one\";\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/DispatchedSubclassTwo.java",
    "content": "package io.wispforest.uwu.network;\n\npublic record DispatchedSubclassTwo(int a) implements DispatchedInterface {\n    @Override\n    public String getName() {\n        return \"two\";\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/KeycodePacket.java",
    "content": "package io.wispforest.uwu.network;\n\npublic record KeycodePacket(Integer key) {}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/MaldingPacket.java",
    "content": "package io.wispforest.uwu.network;\n\npublic record MaldingPacket(DispatchedInterface value) {\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/NullablePacket.java",
    "content": "package io.wispforest.uwu.network;\n\nimport io.wispforest.endec.annotations.NullableComponent;\n\nimport java.util.List;\n\npublic record NullablePacket(@NullableComponent String name, @NullableComponent List<String> names) {}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/SealedSubclassOne.java",
    "content": "package io.wispforest.uwu.network;\n\npublic record SealedSubclassOne(String a, int b) implements SealedTestClass {\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/SealedSubclassTwo.java",
    "content": "package io.wispforest.uwu.network;\n\npublic record SealedSubclassTwo(long a, Void b) implements SealedTestClass {\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/SealedTestClass.java",
    "content": "package io.wispforest.uwu.network;\n\nimport io.wispforest.endec.annotations.SealedPolymorphic;\n\n@SealedPolymorphic\npublic sealed interface SealedTestClass permits SealedSubclassOne, SealedSubclassTwo {\n}"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/StringPacket.java",
    "content": "package io.wispforest.uwu.network;\n\npublic record StringPacket(String value) {}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/UwuNetworkExample.java",
    "content": "package io.wispforest.uwu.network;\n\nimport io.wispforest.endec.impl.RecordEndec;\nimport io.wispforest.endec.impl.ReflectiveEndecBuilder;\nimport io.wispforest.owo.network.OwoNetChannel;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.StructEndec;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;\nimport net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;\nimport net.minecraft.client.KeyMapping;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.lwjgl.glfw.GLFW;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class UwuNetworkExample {\n    public static final Map<String, StructEndec<? extends DispatchedInterface>> REGISTRY = new HashMap<>();\n    public static final OwoNetChannel CHANNEL = OwoNetChannel.create(Identifier.fromNamespaceAndPath(\"uwu\", \"main\"));\n\n    public static void init() {\n        CHANNEL.addEndecs(builder -> {\n            builder.register(Endec.dispatchedStruct(REGISTRY::get, DispatchedInterface::getName, Endec.STRING), DispatchedInterface.class);\n        });\n\n        REGISTRY.put(\"one\", RecordEndec.create(CHANNEL.builder(), DispatchedSubclassOne.class));\n        REGISTRY.put(\"two\", RecordEndec.create(CHANNEL.builder(), DispatchedSubclassTwo.class));\n\n        CHANNEL.registerClientbound(StringPacket.class, (message, access) -> {\n            access.player().displayClientMessage(Component.nullToEmpty(message.value()), false);\n        });\n\n        CHANNEL.registerServerbound(KeycodePacket.class, (message, access) -> {\n            CHANNEL.serverHandle(access.player()).send(new StringPacket(\"Key \" + message.key() + \" pressed\"));\n        });\n\n        CHANNEL.registerServerbound(MaldingPacket.class, (message, access) -> {\n            access.player().displayClientMessage(Component.nullToEmpty(message.toString()), false);\n        });\n\n        CHANNEL.registerServerbound(NullablePacket.class, (message, access) -> {\n            if(message.name() == null && message.names() == null) {\n                access.player().sendSystemMessage(Component.nullToEmpty(\"NULLABLITY FOR THE WIN\"));\n            } else {\n                var text = Component.literal(\"\");\n\n                text.append(Component.nullToEmpty(String.valueOf(message.name())));\n                text.append(Component.nullToEmpty(String.valueOf(message.names())));\n\n                access.player().sendSystemMessage(text);\n            }\n        });\n    }\n\n    @Environment(EnvType.CLIENT)\n    public static final class Client {\n        public static final KeyMapping NETWORK_TEST = new KeyMapping(\"key.uwu.network_test\", GLFW.GLFW_KEY_U, KeyMapping.Category.MISC);\n\n        public static void init() {\n            KeyBindingHelper.registerKeyBinding(NETWORK_TEST);\n            ClientTickEvents.END_CLIENT_TICK.register(client -> {\n                while (NETWORK_TEST.consumeClick()) {\n                    CHANNEL.clientHandle().send(new KeycodePacket(KeyBindingHelper.getBoundKeyOf(NETWORK_TEST).getValue()));\n\n                    CHANNEL.clientHandle().send(new MaldingPacket(new DispatchedSubclassOne(\"base\")));\n                    CHANNEL.clientHandle().send(new MaldingPacket(new DispatchedSubclassTwo(20)));\n\n                    CHANNEL.clientHandle().send(new NullablePacket(null, null));\n                    CHANNEL.clientHandle().send(new NullablePacket(\"Weeee\", null));\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/UwuNetworkTest.java",
    "content": "package io.wispforest.uwu.network;\n\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.format.bytebuf.ByteBufDeserializer;\nimport io.wispforest.endec.format.bytebuf.ByteBufSerializer;\nimport io.wispforest.endec.impl.RecordEndec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport net.fabricmc.fabric.api.networking.v1.PacketByteBufs;\n\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.function.BiPredicate;\nimport java.util.function.Function;\n\npublic class UwuNetworkTest {\n\n    public static void main(String[] args) {\n        var test = new TestRecord(new LinkedList<>(List.of(\"hahayes epic text\")), TestEnum.ANOTHER_VALUE);\n        var serializer = RecordEndec.createShared(TestRecord.class);\n        var sameSerializer = RecordEndec.createShared(TestRecord.class);\n\n        testEquals(serializer, sameSerializer);\n\n        testSerialization(test, testRecord -> {\n            var buffer = PacketByteBufs.create();\n            buffer.write(serializer, test);\n            return buffer.read(serializer);\n        });\n\n        //--\n\n        System.out.println();\n\n        var endec = RecordEndec.createShared(TestRecord.class);\n        var sameendec = RecordEndec.createShared(TestRecord.class);\n\n        testEquals(endec, sameendec);\n\n        testSerialization(test, testRecord -> {\n            return endec.decodeFully(ByteBufDeserializer::of, endec.encodeFully(() -> ByteBufSerializer.of(PacketByteBufs.create()), testRecord));\n        });\n\n        //--\n\n        System.out.println();\n\n        var builtendec = StructEndecBuilder.of(\n                Endec.STRING.listOf().xmap(s -> s, s -> s).fieldOf(\"text\", TestRecord::text),\n                Endec.forEnum(TestEnum.class).fieldOf(\"enumValue\", TestRecord::enumValue),\n                TestRecord::new\n        );\n\n        testSerialization(test, testRecord -> {\n            return builtendec.decodeFully(ByteBufDeserializer::of, builtendec.encodeFully(() -> ByteBufSerializer.of(PacketByteBufs.create()), testRecord));\n        });\n    }\n\n    public static void testSerialization(TestRecord test, Function<TestRecord, TestRecord> function){\n        var read = function.apply(test);\n\n        testEquals(test, read);\n    }\n\n    public record TestRecord(List<String> text, TestEnum enumValue) {}\n\n    public enum TestEnum {ONE_VALUE, ANOTHER_VALUE}\n\n    private static <T> void testEquals(T object, T other) {\n        testEquals(object, other, Objects::toString, Object::equals);\n    }\n\n    private static <T> void testEquals(T object, T other, Function<T, String> formatter, BiPredicate<T, T> predicate) {\n        System.out.println(\"Comparing '\" + formatter.apply(object) + \"' to '\" + formatter.apply(other) + \"'\");\n        System.out.println(\"object == other -> \" + (object == other));\n        System.out.println(\"predicate.test(object, other) -> \" + predicate.test(object, other));\n    }\n\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/network/UwuOptionalNetExample.java",
    "content": "package io.wispforest.uwu.network;\n\nimport io.wispforest.owo.network.OwoNetChannel;\nimport net.fabricmc.api.EnvType;\nimport net.fabricmc.api.Environment;\nimport net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;\nimport net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;\nimport net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.minecraft.client.KeyMapping;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.lwjgl.glfw.GLFW;\n\nimport static net.minecraft.commands.Commands.literal;\n\npublic class UwuOptionalNetExample {\n    public static final boolean SERVER_CHANNEL_IN_CLIENT = false;\n    public static final boolean CLIENT_CHANNEL_IN_SERVER = false;\n\n    public static void init() {\n        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER || SERVER_CHANNEL_IN_CLIENT) {\n            var serverChannel = OwoNetChannel.createOptional(Identifier.fromNamespaceAndPath(\"uwu\", \"optional_server\"));\n\n            serverChannel.registerClientbound(StringPacket.class, (message, access) -> {\n                access.player().displayClientMessage(Component.nullToEmpty(message.value()), false);\n            });\n\n            CommandRegistrationCallback.EVENT.register((dispatcher, access, environment) -> {\n                dispatcher.register(literal(\"test_optional_channels\")\n                        .executes(context -> {\n                            ServerPlayer player = context.getSource().getPlayer();\n\n                            if (serverChannel.canSendToPlayer(player))\n                                serverChannel.serverHandle(player).send(new StringPacket(\"Based™\"));\n\n                            return 0;\n                        }));\n            });\n\n            if (CLIENT_CHANNEL_IN_SERVER) {\n                var clientChannel = OwoNetChannel.createOptional(Identifier.fromNamespaceAndPath(\"uwu\", \"optional_client\"));\n\n                clientChannel.registerServerbound(KeycodePacket.class, (message, access) -> {\n                    System.out.println(message.key());\n                });\n            }\n        }\n    }\n\n    @Environment(EnvType.CLIENT)\n    public static final class Client {\n        public static final KeyMapping NETWORK_TEST = new KeyMapping(\"key.uwu.network_opt_test\", GLFW.GLFW_KEY_M, KeyMapping.Category.MISC);\n\n        public static void init() {\n            var clientChannel = OwoNetChannel.createOptional(Identifier.fromNamespaceAndPath(\"uwu\", \"optional_client\"));\n\n            clientChannel.registerServerbound(KeycodePacket.class, (message, access) -> {\n                System.out.println(message.key());\n            });\n\n            KeyBindingHelper.registerKeyBinding(NETWORK_TEST);\n            ClientTickEvents.END_CLIENT_TICK.register(client -> {\n                while (NETWORK_TEST.consumeClick()) {\n                    if (clientChannel.canSendToServer()) {\n                        clientChannel.clientHandle().send(new KeycodePacket(KeyBindingHelper.getBoundKeyOf(NETWORK_TEST).getValue()));\n                    } else {\n                        client.player.displayClientMessage(Component.nullToEmpty(\"channel unavailable\"), false);\n                    }\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/recipe/UwuShapedRecipe.java",
    "content": "package io.wispforest.uwu.recipe;\n\n//public class UwuShapedRecipe extends ShapedRecipe {\n//\n//    public static RecipeSerializer<UwuShapedRecipe> RECIPE_SERIALIZER;\n//\n//    public UwuShapedRecipe(String group, CraftingRecipeCategory category, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result, boolean showNotification) {\n//        super(group, category, width, height, ingredients, result, showNotification);\n//    }\n//\n//    @Override\n//    public RecipeSerializer<?> getSerializer() {\n//        return RECIPE_SERIALIZER;\n//    }\n//\n//    public static void init() {\n//        RECIPE_SERIALIZER = Registry.register(\n//                Registries.RECIPE_SERIALIZER,\n//                Identifier.of(\"uwu:crafting_shaped\"),\n//                new EndecRecipeSerializer<>(ENDEC)\n//        );\n//    }\n//\n//    //--\n//\n//    private static final Endec<UwuShapedRecipe> FROM_RAW_RECIPE = RawShapedRecipe.ENDEC.xmap(recipe -> {\n//        String[] strings = ShapedRecipeInvoker.owo$removePadding(recipe.pattern);\n//        int i = strings[0].length();\n//        int j = strings.length;\n//        DefaultedList<Ingredient> defaultedList = DefaultedList.ofSize(i * j, Ingredient.EMPTY);\n//        Set<String> set = Sets.newHashSet(recipe.key.keySet());\n//\n//        for (int k = 0; k < strings.length; ++k) {\n//            String string = strings[k];\n//\n//            for (int l = 0; l < string.length(); ++l) {\n//                String string2 = string.substring(l, l + 1);\n//                Ingredient ingredient = string2.equals(\" \") ? Ingredient.EMPTY : (Ingredient) recipe.key.get(string2);\n//\n//                if (ingredient == null) {\n//                    throw new IllegalStateException(\"Pattern references symbol '\" + string2 + \"' but it's not defined in the key\");\n//                }\n//\n//                set.remove(string2);\n//                defaultedList.set(l + i * k, ingredient);\n//            }\n//        }\n//\n//        if (!set.isEmpty()) throw new IllegalStateException(\"Key defines symbols that aren't used in pattern: \" + set);\n//\n//        return new UwuShapedRecipe(recipe.group, recipe.category, i, j, defaultedList, recipe.result, recipe.showNotification);\n//    }, recipe -> {\n//        throw new NotImplementedException(\"Serializing ShapedRecipe is not implemented yet.\");\n//    });\n//\n//    private static final Endec<DefaultedList<Ingredient>> INGREDIENTS = Endec.ofCodec(Ingredient.ALLOW_EMPTY_CODEC)\n//            .listOf()\n//            .xmap(ingredients -> new DefaultedList<>(ingredients, null) {}, defaulted -> defaulted);\n//\n//    private static final Endec<UwuShapedRecipe> FROM_INSTANCE = StructEndecBuilder.of(\n//            Endec.STRING.fieldOf(\"group\", ShapedRecipe::getGroup),\n//            Endec.ofCodec(CraftingRecipeCategory.CODEC).fieldOf(\"category\", ShapedRecipe::getCategory),\n//            Endec.VAR_INT.fieldOf(\"width\", ShapedRecipe::getWidth),\n//            Endec.VAR_INT.fieldOf(\"height\", ShapedRecipe::getHeight),\n//            INGREDIENTS.fieldOf(\"ingredients\", ShapedRecipe::getIngredients),\n//            Endec.ofCodec(ItemStack.RECIPE_RESULT_CODEC).fieldOf(\"result\", recipe -> recipe.getResult(null)),\n//            Endec.BOOLEAN.fieldOf(\"show_notification\", ShapedRecipe::showNotification),\n//            UwuShapedRecipe::new\n//    );\n//\n//    private static final Endec<UwuShapedRecipe> ENDEC = new AttributeEndecBuilder<>(FROM_RAW_RECIPE, SerializationAttribute.HUMAN_READABLE)\n//            .orElse(FROM_INSTANCE);\n//\n//    //--\n//\n//\n//    private record RawShapedRecipe(String group, CraftingRecipeCategory category, Map<String, Ingredient> key,\n//                                   List<String> pattern, ItemStack result, boolean showNotification) {\n//        private static final Endec<List<String>> PATTERN_ENDEC = Endec.STRING.listOf().validate(rows -> {\n//            if (rows.size() > 3) throw new IllegalStateException(\"Invalid pattern: too many rows, 3 is maximum\");\n//            if (rows.isEmpty()) throw new IllegalStateException(\"Invalid pattern: empty pattern not allowed\");\n//\n//            int i = rows.get(0).length();\n//\n//            for (String string : rows) {\n//                if (string.length() > 3) {\n//                    throw new IllegalStateException(\"Invalid pattern: too many columns, 3 is maximum\");\n//                }\n//                if (i != string.length()) {\n//                    throw new IllegalStateException(\"Invalid pattern: each row must be the same width\");\n//                }\n//            }\n//        });\n//\n//        public static final Endec<RawShapedRecipe> ENDEC = StructEndecBuilder.of(\n//                Endec.STRING.optionalFieldOf(\"group\", recipe -> recipe.group, \"\"),\n//                Endec.ofCodec(CraftingRecipeCategory.CODEC).optionalFieldOf(\"category\", recipe -> recipe.category, CraftingRecipeCategory.MISC),\n//                Endec.ofCodec(Ingredient.DISALLOW_EMPTY_CODEC).mapOf().validate(map -> {\n//                    for (var key : map.keySet()) {\n//                        if (key.length() != 1) {\n//                            throw new IllegalStateException(\"Invalid key entry: '\" + key + \"' is an invalid symbol (must be 1 character only).\");\n//                        } else if (\" \".equals(key)) {\n//                            throw new IllegalStateException(\"Invalid key entry: ' ' is a reserved symbol.\");\n//                        }\n//                    }\n//                }).fieldOf(\"key\", recipe -> recipe.key),\n//                PATTERN_ENDEC.fieldOf(\"pattern\", recipe -> recipe.pattern),\n//                Endec.ofCodec(ItemStack.RECIPE_RESULT_CODEC).fieldOf(\"result\", recipe -> recipe.result),\n//                Endec.BOOLEAN.optionalFieldOf(\"show_notification\", recipe -> recipe.showNotification, true),\n//                RawShapedRecipe::new\n//        );\n//    }\n//}"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/rei/UiCategory.java",
    "content": "package io.wispforest.uwu.rei;\n\nimport io.wispforest.owo.Owo;\nimport io.wispforest.owo.compat.rei.ReiUIAdapter;\nimport io.wispforest.owo.ui.component.ButtonComponent;\nimport io.wispforest.owo.ui.component.UIComponents;\nimport io.wispforest.owo.ui.container.UIContainers;\nimport io.wispforest.owo.ui.core.*;\nimport io.wispforest.uwu.items.UwuItems;\nimport me.shedaniel.math.Point;\nimport me.shedaniel.math.Rectangle;\nimport me.shedaniel.rei.api.client.gui.Renderer;\nimport me.shedaniel.rei.api.client.gui.widgets.Widget;\nimport me.shedaniel.rei.api.client.gui.widgets.Widgets;\nimport me.shedaniel.rei.api.client.registry.display.DisplayCategory;\nimport me.shedaniel.rei.api.common.category.CategoryIdentifier;\nimport me.shedaniel.rei.api.common.display.Display;\nimport me.shedaniel.rei.api.common.display.DisplaySerializer;\nimport me.shedaniel.rei.api.common.entry.EntryIngredient;\nimport me.shedaniel.rei.api.common.util.EntryIngredients;\nimport me.shedaniel.rei.api.common.util.EntryStacks;\nimport net.minecraft.world.item.Items;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.resources.Identifier;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class UiCategory implements DisplayCategory<Display> {\n\n    public static CategoryIdentifier<UiDisplay> ID = CategoryIdentifier.of(Owo.id(\"ui\"));\n\n    @Override\n    public List<Widget> setupDisplay(Display display, Rectangle bounds) {\n        var adapter = new ReiUIAdapter<>(bounds, UIContainers::verticalFlow);\n        var root = adapter.rootComponent();\n\n        root.horizontalAlignment(HorizontalAlignment.CENTER)\n                .surface(Surface.DARK_PANEL)\n                .padding(Insets.of(8));\n\n        var inner = UIContainers.verticalFlow(Sizing.fill(100), Sizing.content());\n        inner.horizontalAlignment(HorizontalAlignment.CENTER).surface(Surface.flat(0xFF00FFAF));\n\n        inner.child(UIComponents.label(Component.nullToEmpty(\"A demonstration\\ninside REI\"))\n                .color(Color.BLACK)\n                .positioning(Positioning.absolute(3, 3))\n        );\n\n        var animation = inner.horizontalSizing().animate(250, Easing.QUADRATIC, Sizing.fill(65));\n        inner.child(UIComponents.button(Component.nullToEmpty(\"shrink\"), (ButtonComponent button) -> animation.forwards())\n                .margins(Insets.vertical(25))\n                .horizontalSizing(Sizing.fixed(60)));\n        inner.child(UIComponents.button(Component.nullToEmpty(\"grow\"), (ButtonComponent button) -> animation.backwards())\n                .margins(Insets.vertical(25))\n                .horizontalSizing(Sizing.fixed(60)));\n\n        inner.child(adapter.wrap(Widgets.createSlot(new Point(0, 0)).entry(EntryStacks.of(Items.ECHO_SHARD))));\n\n        root.child(UIContainers.verticalScroll(Sizing.content(), Sizing.fill(100), inner));\n\n        adapter.prepare();\n        return List.of(adapter);\n    }\n\n    @Override\n    public Renderer getIcon() {\n        return EntryStacks.of(Items.ECHO_SHARD);\n    }\n\n    @Override\n    public Component getTitle() {\n        return Component.nullToEmpty(\"yes its gui very epic\");\n    }\n\n    @Override\n    public CategoryIdentifier<? extends Display> getCategoryIdentifier() {\n        return ID;\n    }\n\n    public static class UiDisplay implements Display {\n\n        @Override\n        public List<EntryIngredient> getInputEntries() {\n            return List.of(EntryIngredients.of(UwuItems.SCREEN_SHARD));\n        }\n\n        @Override\n        public List<EntryIngredient> getOutputEntries() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public CategoryIdentifier<?> getCategoryIdentifier() {\n            return ID;\n        }\n\n        @Override\n        public Optional<Identifier> getDisplayLocation() {\n            return Optional.empty();\n        }\n\n        @Override\n        public @Nullable DisplaySerializer<? extends Display> getSerializer() {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/rei/UwuReiPlugin.java",
    "content": "package io.wispforest.uwu.rei;\n\nimport io.wispforest.owo.braid.core.BraidScreen;\nimport me.shedaniel.math.Rectangle;\nimport me.shedaniel.rei.api.client.plugins.REIClientPlugin;\nimport me.shedaniel.rei.api.client.registry.category.CategoryRegistry;\nimport me.shedaniel.rei.api.client.registry.display.DisplayRegistry;\nimport me.shedaniel.rei.api.client.registry.screen.DisplayBoundsProvider;\nimport me.shedaniel.rei.api.client.registry.screen.ScreenRegistry;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.world.InteractionResult;\nimport org.jetbrains.annotations.Nullable;\n\npublic class UwuReiPlugin implements REIClientPlugin {\n\n    @Override\n    public void registerCategories(CategoryRegistry registry) {\n        registry.add(new UiCategory());\n    }\n\n    @Override\n    public void registerDisplays(DisplayRegistry registry) {\n        registry.add(new UiCategory.UiDisplay());\n    }\n\n    @Override\n    public void registerScreens(ScreenRegistry registry) {\n        registry.registerDecider(new DisplayBoundsProvider<BraidScreen>() {\n            @Override\n            public @Nullable Rectangle getScreenBounds(BraidScreen screen) {\n                return new Rectangle(screen.width / 4, screen.height / 4, screen.width / 2, screen.height / 2);\n            }\n\n            @Override\n            public <R extends Screen> boolean isHandingScreen(Class<R> screen) {\n                return BraidScreen.class.isAssignableFrom(screen);\n            }\n\n            @Override\n            public <R extends Screen> InteractionResult shouldScreenBeOverlaid(R screen) {\n                return InteractionResult.SUCCESS;\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/testmod/java/io/wispforest/uwu/text/BasedTextContent.java",
    "content": "package io.wispforest.uwu.text;\n\nimport com.mojang.serialization.MapCodec;\nimport io.wispforest.endec.Endec;\nimport io.wispforest.endec.impl.StructEndecBuilder;\nimport io.wispforest.owo.serialization.CodecUtils;\nimport net.minecraft.network.chat.ComponentContents;\nimport net.minecraft.network.chat.FormattedText;\nimport net.minecraft.network.chat.Style;\n\nimport java.util.Optional;\n\npublic class BasedTextContent implements ComponentContents {\n\n    public static final MapCodec<BasedTextContent> CODEC = CodecUtils.toMapCodec(StructEndecBuilder.of(Endec.STRING.fieldOf(\"based\", o -> o.basedText), BasedTextContent::new));\n\n    private final String basedText;\n\n    public BasedTextContent(String basedText) {\n        this.basedText = basedText;\n    }\n\n    @Override\n    public <T> Optional<T> visit(FormattedText.ContentConsumer<T> visitor) {\n        return visitor.accept(\"I am extremely based: \" + basedText);\n    }\n\n    @Override\n    public <T> Optional<T> visit(FormattedText.StyledContentConsumer<T> visitor, Style style) {\n        return visitor.accept(style, \"I am extremely based: \" + basedText);\n    }\n\n    @Override\n    public MapCodec<? extends ComponentContents> codec() {\n        return CODEC;\n    }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uowou/items/owo_ingot.json5",
    "content": "{\n  model: {\n    type: \"minecraft:model\",\n    model: \"uowou:item/owo_ingot\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uowou/models/item/owo_ingot.json5",
    "content": "{\n  parent: \"item/generated\",\n  textures: {\n    layer0: \"uowou:item/owo_ingot\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/blockstates/braid_display.json5",
    "content": "{\n  variants: {\n    \"\": {\n      model: \"uwu:block/braid_display\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/items/braid.json5",
    "content": "{\n  model: {\n    type: \"minecraft:model\",\n    model: \"uwu:item/braid\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/items/braid_display.json5",
    "content": "{\n\tmodel: {\n\t\ttype: \"minecraft:model\",\n\t\tmodel: \"uwu:block/braid_display\"\n\t}\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/items/braid_samples.json5",
    "content": "{\n  model: {\n    type: \"minecraft:model\",\n    model: \"uwu:item/braid\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/items/screen_shard.json",
    "content": "{\n  \"model\": {\n    \"type\": \"minecraft:model\",\n    \"model\": \"minecraft:item/echo_shard\"\n  }\n}"
  },
  {
    "path": "src/testmod/resources/assets/uwu/items/screen_shard.json5",
    "content": "{\n  model: {\n    type: \"minecraft:model\",\n    model: \"minecraft:item/echo_shard\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/items/test_stick.json",
    "content": "{\n  \"model\": {\n    \"type\": \"minecraft:model\",\n    \"model\": \"minecraft:item/stick\"\n  }\n}"
  },
  {
    "path": "src/testmod/resources/assets/uwu/items/test_stick.json5",
    "content": "{\n  model: {\n    type: \"minecraft:model\",\n    model: \"minecraft:item/stick\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/lang/en_us.json5",
    "content": "{\n  \"block.uwu.braid_display\": [\n    {translate: \"text.uwu.braid\"},\n    \" display\"\n  ],\n\n  \"item.{}\": {\n    \"uwu.{}\": {\n      counter: \"Counter\",\n      test_stick: [\n        \"\",\n        {text: \"Test\", color: \"green\"},\n        {text: \" Stick\", color: \"red\"}\n      ],\n      screen_shard: [\n        {text: \"Screen Shard\", color: \"#3AB0FF\"}\n      ],\n      braid: [\n        {translate: \"text.uwu.braid\"},\n        {text: \" (chyz tried)\", color: \"#FFFFFF\"}\n      ],\n      braid_samples: [\n        {translate: \"text.uwu.braid\"},\n        {text: \" samples\", color: \"#FFFFFF\"}\n      ]\n    },\n    \"uowou.owo_ingot\": [\n      {text: \"uωu\", color: \"#3AB0FF\"},\n      {text: \" Ingot\", color: \"#FFFFFF\"}\n    ]\n  },\n  \"itemGroup.{}\": {\n    \"food_and_drink.button.consume\": \"C o n s u m e\",\n    \"ingredients.tab.{}\": {\n      the_logs: \"The Logs\",\n      extra_ores: \"Extra Ores\"\n    },\n    \"uwu.{}\": {\n      \"four_tab_group.{}\": {\n        \"\": \"uωu\",\n        \"tab.tab_{}\": [\n          \"Tab 1\",\n          \"Tab 2\",\n          \"Tab 3\",\n          \"Tab 4\",\n          \"Tab 5\"\n        ],\n        \"button.github\": \"oωo GitHub\"\n      },\n      \"six_tab_group.{}\": {\n        \"\": \"uωu\",\n        \"tab.tab_{}\": [\n          \"Tab 1\",\n          \"Tab 2\",\n          \"Tab 3\",\n          \"Tab 4\",\n          \"Tab 5\",\n          \"Tab 6\"\n        ],\n        \"button.owo\": \"ouωuo\"\n      },\n      \"single_tab_group.{}\": {\n        \"\": \"uωu\",\n        \"tab.tab_1\": \"uωu~\"\n      },\n      \"vanilla_group.{}\": {\n        \"\": \"uωu (.❛ ᴗ ❛.)\",\n        \"tab.tab_{0}\": [\n          \"(´• ω •`)\",\n          \"(o˘◡˘o)\"\n        ]\n      }\n    }\n  },\n  /*\n  I'm\n  honestly\n  just\n  writing\n  comments\n  cuz\n  I\n  can\n  */\n\n  \"key.uwu.hud_test\": [\n    {text: \"Toggle Test HUD\", color: \"#0096FF\"}\n  ],\n\n  \"menu.returnTo{}\": {\n    Game: {\n      text: \"Back to Game\",\n      color: \"#FF5272\"\n    },\n    Menu: [\n      \"\",\n      {text: \"Save\", color: \"#52BDFF\"},\n      {text: \" and\", color: \"#52FFAA\"},\n      {text: \" Quit\", color: \"#FFEF52\"},\n      {text: \" to\", color: \"#FF9052\"},\n      {text: \" Title\", color: \"#FF5272\"}\n    ]\n  },\n\n  \"text.{}\": {\n    \"uwu.{}\": {\n      chyz: [\n        {text: \"c\", color: \"#fdf433\"},\n        {text: \"h\", color: \"#fffdff\"},\n        {text: \"y\", color: \"#9b57d0\"},\n        {text: \"z\", color: \"#2f2f2f\"}\n      ],\n      glisco: [\n        {text: \"g\", color: \"#ff7166\"},\n        {text: \"l\", color: \"#ffb266\"},\n        {text: \"i\", color: \"#ceff66\"},\n        {text: \"s\", color: \"#90ff66\"},\n        {text: \"c\", color: \"#66d4ff\"},\n        {text: \"o\", color: \"#c366ff\"}\n      ],\n      braid: [\n        {text: \"b\", color: \"#ff7166\"},\n        {text: \"r\", color: \"#ffb266\"},\n        {text: \"a\", color: \"#ceff66\"},\n        {text: \"i\", color: \"#90ff66\"},\n        {text: \"d\", color: \"#66d4ff\"}\n      ]\n    },\n    \"config.{}\": {\n      \"uwu.{}\": {\n        title: \"Test Config Screen\",\n        \"section.{}\": {\n          top: \"Top Text\",\n          bottom: \"Bottom Text\",\n          \"nesting_yo?\": \"That's Nestnite\"\n        },\n        \"category.{}\": {\n          \"nestingTime.{}\": {\n            \"\": \"Nesting Time\",\n            tooltip: \"Yep, it's nesting Time\",\n            nestingTimeIntensifies: \"Nesting Time Intensifies\"\n          }\n        },\n        \"option.{}\": {\n          aValue: \"A Value\",\n          \"regex.{}\": {\n            \"\": \"Regex'd\",\n            tooltip: \"Regex tooltip, this is epic\"\n          },\n          \"nestingTime.{}\": {\n            togglee: \"Togglee\",\n            yesThisIsAlsoNested: \"Yes this is also nested\",\n            \"nestedIntegers.{}\": {\n              \"\": \"A nested List of Integers\",\n              tooltip: [\n                \"A very epic nested Integers tooltip\",\n                {text: \"\\nwith multiple lines\", color: \"#ae0000\"}\n              ]\n            },\n            \"nestingTimeIntensifies.wowSoNested\": \"Wow so nested\"\n          },\n          someOption: \"Hello, yes, this is the predicate constraint tutorial\",\n          \"floting.{}\": {\n            \"\": \"Floting\",\n            tooltip: \"messes with button?\"\n          },\n          thisIsAStringValue: \"Clearly, this is a string value\",\n          thereAreStringsHere: \"There are Strings here\",\n          \"brotheresAnEnum.{}\": {\n            \"\": \"Bro, there's an Enum!\",\n            tooltip: \"Yeah, it's crazy\"\n          },\n          \"anEpicColor.{}\": {\n            \"\": \"An epic color\",\n            WithAlpha: \"An epic color with alpha\"\n          }\n        },\n        \"enum.wowValues.{}\": {\n          first: \"First\",\n          second: \"Second\",\n          third: \"Third\",\n          fourth: \"Fourth\"\n        }\n      },\n      \"uowou.option.thisIsNotSyncable\": \"Unsyncable Option\",\n    },\n    \"ui.test_slider\": \"Value: %s\"\n  },\n\n  \"uwu.{}\": {\n    a: {index: 0, color: \"#ae0000\"},\n    homicide: \"homicide\",\n    eepy: \"honk mimimimimimimimimi\"\n  },\n\n  //Nested lang tests\n\n  \"nestedLangTests.{}\": {\n    \"prefixTest.{}\": {\n      prefixTestKey: \"nestedLangTests.prefixTest.prefixTestKey\"\n    },\n    \"middleTest.{}.Suffix\": {\n      middleTestKey: \"nestedLangTests.middleTest.middleTestKey.Suffix\"\n    },\n    \"{}.suffixTest\": {\n      suffixTestKey: \"nestedLangTests.suffixTestKey.suffixTest\"\n    },\n\n    \"listPrefixTest.{}\": [\n      \"nestedLangTests.listPrefixTest.1\",\n      \"nestedLangTests.listPrefixTest.2\"\n    ],\n    \"listPrefixTestWithIndex.{2}\": [\n      \"nestedLangTests.listPrefixTestWithIndex.2\",\n      \"nestedLangTests.listPrefixTestWithIndex.3\"\n    ],\n    \"listMiddleTest.{}.Suffix\": [\n      \"nestedLangTests.listMiddleTest.1.Suffix\",\n      \"nestedLangTests.listMiddleTest.2.Suffix\"\n    ],\n    \"listMiddleTestWithIndex.{2}.Suffix\": [\n      \"nestedLangTests.listMiddleTestWithIndex.2.Suffix\",\n      \"nestedLangTests.listMiddleTestWithIndex.3.Suffix\"\n    ],\n    \"{}.ListSuffixTest\": [\n      \"nestedLangTests.1.ListSuffixTest\",\n      \"nestedLangTests.2.ListSuffixTest\"\n    ],\n    \"{2}.ListSuffixTestWithIndex\": [\n      \"nestedLangTests.2.ListSuffixTestWithIndex\",\n      \"nestedLangTests.3.ListSuffixTestWithIndex\"\n    ],\n    \"escapeTest./+=_!{}\": {\n      \"\": \"nestedLangTests.escapeTest\",\n    }\n  },\n\n  \"This key is here to trigger owo language loading fail prevention\": {\n    \"does it work?\": \"probably\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/models/block/braid_display.json5",
    "content": "{\n\tformat_version: \"1.21.6\",\n\tcredit: \"Made with Blockbench\",\n\tparent: \"block/block\",\n\ttextures: {\n\t\t\"0\": \"uwu:block/braid_display\",\n\t\tparticle: \"uwu:block/braid_display\"\n\t},\n\telements: [\n\t\t{\n\t\t\tfrom: [0, 0, 0],\n\t\t\tto: [16, 2, 16],\n\t\t\trotation: {angle: 0, axis: \"y\", origin: [7, 0, 7]},\n\t\t\tfaces: {\n\t\t\t\tnorth: {uv: [0, 0, 16, 1], rotation: 180, texture: \"#0\"},\n\t\t\t\teast: {uv: [0, 0, 16, 1], texture: \"#0\"},\n\t\t\t\tsouth: {uv: [0, 0, 1, 16], rotation: 90, texture: \"#0\"},\n\t\t\t\twest: {uv: [0, 0, 16, 1], texture: \"#0\"},\n\t\t\t\tup: {uv: [0, 0, 16, 16], texture: \"#0\"},\n\t\t\t\tdown: {uv: [0, 0, 16, 16], texture: \"#0\"}\n\t\t\t}\n\t\t}\n\t],\n\tdisplay: {\n\t\tthirdperson_righthand: {\n\t\t\trotation: [75, 45, 0],\n\t\t\ttranslation: [0, 2.5, 2.75],\n\t\t\tscale: [0.375, 0.375, 0.375]\n\t\t},\n\t\tthirdperson_lefthand: {\n\t\t\trotation: [75, 45, 0],\n\t\t\ttranslation: [0, 2.5, 2.75],\n\t\t\tscale: [0.375, 0.375, 0.375]\n\t\t},\n\t\tfirstperson_righthand: {\n\t\t\trotation: [0, 45, 0],\n\t\t\ttranslation: [0, 2.75, 0],\n\t\t\tscale: [0.4, 0.4, 0.4]\n\t\t},\n\t\tfirstperson_lefthand: {\n\t\t\trotation: [0, -135, 0],\n\t\t\ttranslation: [0, 2.75, 0],\n\t\t\tscale: [0.4, 0.4, 0.4]\n\t\t},\n\t\tground: {\n\t\t\ttranslation: [0, 3, 0],\n\t\t\tscale: [0.25, 0.25, 0.25]\n\t\t},\n\t\tgui: {\n\t\t\trotation: [30, -135, 0],\n\t\t\tscale: [0.625, 0.625, 0.625]\n\t\t},\n\t\tfixed: {\n\t\t\tscale: [0.5, 0.5, 0.5]\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/models/item/braid.json5",
    "content": "{\n  credit: \"Made with Blockbench\",\n  parent: \"item/generated\",\n  textures: {\n    layer0: \"uwu:item/braid\"\n  },\n  display: {\n    gui: {\n      translation: [0, 0.5, 0]\n    }\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/models/item/counter.json5",
    "content": "{\n  parent: \"item/generated\",\n  textures: {\n    layer0: \"item/compass_16\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/nine_patch_textures/contributors_panel.json5",
    "content": "{\n  texture: \"uwu:textures/gui/contributors_panel.png\",\n  texture_width: 16,\n  texture_height: 16,\n  repeat: true,\n  corner_patch_size: {\n    width: 1,\n    height: 1\n  },\n  center_patch_size: {\n    width: 12,\n    height: 12\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/owo_ui/config.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"vertical\">\n            <children>\n                <flow-layout direction=\"vertical\">\n                    <children>\n                        <label id=\"title\">\n                            <text translate=\"true\"/>\n                            <shadow>true</shadow>\n\n                            <margins>\n                                <vertical>10</vertical>\n                            </margins>\n                        </label>\n                    </children>\n\n                    <vertical-alignment>center</vertical-alignment>\n                </flow-layout>\n\n                <stack-layout>\n                    <children>\n                        <flow-layout direction=\"horizontal\" id=\"main-panel\">\n                            <children>\n                                <flow-layout direction=\"vertical\" id=\"option-panel-container\">\n                                    <children>\n                                        <scroll direction=\"vertical\" id=\"option-panel-scroll\">\n                                            <flow-layout direction=\"vertical\" id=\"option-panel\">\n                                                <children/>\n                                                <padding>\n                                                    <right>3</right>\n                                                </padding>\n                                            </flow-layout>\n\n                                            <scrollbar-thiccness>3</scrollbar-thiccness>\n\n                                            <sizing>\n                                                <horizontal method=\"fill\">100</horizontal>\n                                                <vertical method=\"fill\">100</vertical>\n                                            </sizing>\n\n                                            <padding>\n                                                <all>1</all>\n                                            </padding>\n                                        </scroll>\n                                    </children>\n\n                                    <sizing>\n                                        <horizontal method=\"expand\">100</horizontal>\n                                        <vertical method=\"fill\">100</vertical>\n                                    </sizing>\n                                </flow-layout>\n                            </children>\n\n                            <padding>\n                                <horizontal>50</horizontal>\n                            </padding>\n\n                            <sizing>\n                                <horizontal method=\"fill\">100</horizontal>\n                                <vertical method=\"fill\">100</vertical>\n                            </sizing>\n\n                            <surface>\n                                <flat>#77000000</flat>\n                                <outline>#99121212</outline>\n                            </surface>\n                        </flow-layout>\n                    </children>\n\n                    <padding>\n                        <all>1</all>\n                    </padding>\n\n                    <sizing>\n                        <horizontal method=\"fill\">101</horizontal>\n                        <vertical method=\"expand\">100</vertical>\n                    </sizing>\n\n                    <surface>\n                        <outline>#33FFFFFF</outline>\n                    </surface>\n                </stack-layout>\n\n                <flow-layout direction=\"horizontal\">\n                    <children>\n                        <flow-layout direction=\"horizontal\">\n                            <children>\n                                <texture texture=\"owo:textures/gui/config_search.png\"\n                                         texture-width=\"16\" texture-height=\"16\"\n                                         region-width=\"16\" region-height=\"16\">\n                                    <margins>\n                                        <all>2</all>\n                                    </margins>\n                                </texture>\n\n                                <flow-layout direction=\"horizontal\">\n                                    <children>\n                                        <text-box id=\"search-field\">\n                                            <show-background>false</show-background>\n                                            <max-length>128</max-length>\n\n                                            <sizing>\n                                                <horizontal method=\"fill\">50</horizontal>\n                                                <vertical method=\"fixed\">9</vertical>\n                                            </sizing>\n                                        </text-box>\n\n                                        <label id=\"search-match-indicator\">\n                                            <margins>\n                                                <horizontal>5</horizontal>\n                                            </margins>\n                                        </label>\n                                    </children>\n\n                                    <surface>\n                                        <vanilla-translucent/>\n                                    </surface>\n\n                                    <vertical-alignment>center</vertical-alignment>\n\n                                    <padding>\n                                        <all>3</all>\n                                    </padding>\n                                </flow-layout>\n                            </children>\n\n                            <vertical-alignment>center</vertical-alignment>\n\n                            <positioning type=\"relative\">0,50</positioning>\n                        </flow-layout>\n\n                        <button id=\"reload-button\">\n                            <text translate=\"true\">text.owo.config.button.reload</text>\n                            <sizing>\n                                <horizontal method=\"fill\">10</horizontal>\n                            </sizing>\n                            <margins>\n                                <right>5</right>\n                            </margins>\n                        </button>\n\n                        <button id=\"done-button\">\n                            <text translate=\"true\">text.owo.config.button.done</text>\n                            <sizing>\n                                <horizontal method=\"fill\">10</horizontal>\n                            </sizing>\n                        </button>\n                    </children>\n\n                    <horizontal-alignment>right</horizontal-alignment>\n                    <vertical-alignment>center</vertical-alignment>\n\n                    <margins>\n                        <vertical>5</vertical>\n                    </margins>\n\n                    <padding>\n                        <horizontal>50</horizontal>\n                    </padding>\n\n                    <sizing>\n                        <horizontal method=\"fill\">100</horizontal>\n                    </sizing>\n                </flow-layout>\n            </children>\n\n            <vertical-alignment>center</vertical-alignment>\n            <horizontal-alignment>center</horizontal-alignment>\n\n            <surface>\n                <options-background/>\n            </surface>\n\n            <sizing>\n                <horizontal method=\"fill\">100</horizontal>\n                <vertical method=\"fill\">100</vertical>\n            </sizing>\n        </flow-layout>\n    </components>\n\n    <templates>\n        <template name=\"section-header\">\n            <flow-layout direction=\"horizontal\">\n                <children>\n                    <box>\n                        <sizing>\n                            <vertical method=\"fixed\">2</vertical>\n                            <horizontal method=\"fill\">35</horizontal>\n                        </sizing>\n\n                        <start-color>#FFFFFFFF</start-color>\n                        <end-color>#00000000</end-color>\n\n                        <direction>right-to-left</direction>\n\n                        <fill>true</fill>\n                    </box>\n                    <label id=\"header\">\n                        <margins>\n                            <horizontal>5</horizontal>\n                        </margins>\n                    </label>\n                    <box>\n                        <sizing>\n                            <vertical method=\"fixed\">2</vertical>\n                            <horizontal method=\"fill\">35</horizontal>\n                        </sizing>\n\n                        <start-color>#FFFFFFFF</start-color>\n                        <end-color>#00000000</end-color>\n\n                        <direction>left-to-right</direction>\n\n                        <fill>true</fill>\n                    </box>\n                </children>\n\n                <horizontal-alignment>center</horizontal-alignment>\n                <vertical-alignment>center</vertical-alignment>\n\n                <margins>\n                    <top>10</top>\n                </margins>\n\n                <sizing>\n                    <horizontal method=\"fill\">100</horizontal>\n                    <vertical method=\"fixed\">20</vertical>\n                </sizing>\n            </flow-layout>\n        </template>\n\n        <template name=\"section-buttons\">\n            <flow-layout direction=\"vertical\">\n                <children>\n                    <label>\n                        <text translate=\"true\">text.owo.config.sections</text>\n\n                        <positioning type=\"relative\">50,0</positioning>\n\n                        <margins>\n                            <top>15</top>\n                        </margins>\n                    </label>\n                </children>\n\n                <vertical-alignment>center</vertical-alignment>\n                <horizontal-alignment>center</horizontal-alignment>\n\n                <padding>\n                    <horizontal>2</horizontal>\n                </padding>\n\n                <sizing>\n                    <horizontal method=\"fixed\">0</horizontal>\n                    <vertical method=\"fill\">100</vertical>\n                </sizing>\n            </flow-layout>\n        </template>\n\n        <template name=\"config-option-base\">\n            <flow-layout direction=\"horizontal\">\n                <children>\n                    <label id=\"option-name\">\n                        <text translate=\"true\">{{config-option-name}}</text>\n                        <positioning type=\"relative\">0,50</positioning>\n                        <shadow>true</shadow>\n                    </label>\n\n                    <template-child id=\"controls\">\n                        <positioning type=\"relative\">100,50</positioning>\n                        <vertical-alignment>center</vertical-alignment>\n                    </template-child>\n                </children>\n\n                <sizing>\n                    <horizontal method=\"fill\">100</horizontal>\n                    <vertical method=\"fixed\">32</vertical>\n                </sizing>\n\n                <padding>\n                    <all>5</all>\n                </padding>\n            </flow-layout>\n        </template>\n\n        <template name=\"config-option\">\n            <template name=\"config-option-base\">\n                <child id=\"controls\">\n                    <flow-layout direction=\"horizontal\" id=\"controls-flow\">\n                        <children>\n                            <template-child id=\"value-container\">\n                                <sizing>\n                                    <horizontal method=\"fixed\">120</horizontal>\n                                </sizing>\n                            </template-child>\n\n                            <button id=\"reset-button\">\n                                <text>⇄</text>\n                                <margins>\n                                    <horizontal>5</horizontal>\n                                </margins>\n                            </button>\n                        </children>\n                    </flow-layout>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"text-box-config-option\">\n            <template name=\"config-option\">\n                <child id=\"value-container\">\n                    <config-text-box id=\"value-box\">\n                        <text>{{config-option-value}}</text>\n                    </config-text-box>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"boolean-toggle-config-option\">\n            <template name=\"config-option\">\n                <child id=\"value-container\">\n                    <config-toggle-button id=\"toggle-button\"/>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"enum-config-option\">\n            <template name=\"config-option\">\n                <child id=\"value-container\">\n                    <config-enum-button id=\"enum-button\"/>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"range-config-option\">\n            <template name=\"config-option-base\">\n                <child id=\"controls\">\n                    <flow-layout direction=\"horizontal\" id=\"controls-flow\">\n                        <children>\n                            <button id=\"toggle-button\">\n                                <text>✎</text>\n                                <tooltip-text translate=\"true\">text.owo.config.button.range.edit_as_text</tooltip-text>\n\n                                <renderer>\n                                    <flat color=\"#00000000\" hovered-color=\"#77000000\" disabled-color=\"#00000000\"/>\n                                </renderer>\n\n                                <sizing>\n                                    <vertical method=\"fixed\">16</vertical>\n                                </sizing>\n                            </button>\n\n                            <flow-layout direction=\"horizontal\" id=\"slider-controls\">\n                                <children>\n                                    <config-slider id=\"value-slider\">\n                                        <margins>\n                                            <all>1</all>\n                                        </margins>\n                                        <sizing>\n                                            <horizontal method=\"fixed\">120</horizontal>\n                                        </sizing>\n                                    </config-slider>\n\n                                    <button id=\"reset-button\">\n                                        <text>⇄</text>\n                                        <margins>\n                                            <horizontal>5</horizontal>\n                                        </margins>\n                                    </button>\n                                </children>\n\n                                <vertical-alignment>center</vertical-alignment>\n                            </flow-layout>\n                        </children>\n\n                        <gap>2</gap>\n                    </flow-layout>\n                </child>\n            </template>\n        </template>\n\n        <template name=\"color-picker-panel\">\n            <flow-layout direction=\"vertical\">\n                <children>\n                    <label>\n                        <text>Choose color</text>\n                        <shadow>true</shadow>\n                        <margins>\n                            <top>3</top>\n                        </margins>\n                    </label>\n\n                    <color-picker id=\"color-picker\">\n                        <show-alpha>{{with-alpha}}</show-alpha>\n                        <selected-color>{{color}}</selected-color>\n                        <sizing>\n                            <horizontal method=\"fixed\">160</horizontal>\n                            <vertical method=\"fixed\">100</vertical>\n                        </sizing>\n                    </color-picker>\n\n                    <flow-layout direction=\"horizontal\">\n                        <children>\n                            <box>\n                                <fill>true</fill>\n                                <color>{{color}}</color>\n                                <sizing>\n                                    <horizontal method=\"fixed\">80</horizontal>\n                                    <vertical method=\"fixed\">15</vertical>\n                                </sizing>\n                            </box>\n                            <box id=\"current-color\">\n                                <fill>true</fill>\n                                <color>{{color}}</color>\n                                <sizing>\n                                    <horizontal method=\"fixed\">80</horizontal>\n                                    <vertical method=\"fixed\">15</vertical>\n                                </sizing>\n                            </box>\n                        </children>\n                    </flow-layout>\n\n                    <flow-layout direction=\"horizontal\">\n                        <children>\n                            <button id=\"cancel-button\">\n                                <text>❌</text>\n                                <sizing>\n                                    <horizontal method=\"fixed\">50</horizontal>\n                                    <vertical method=\"fixed\">14</vertical>\n                                </sizing>\n                            </button>\n\n                            <button id=\"confirm-button\">\n                                <text>✔</text>\n                                <sizing>\n                                    <horizontal method=\"fixed\">50</horizontal>\n                                    <vertical method=\"fixed\">14</vertical>\n                                </sizing>\n                            </button>\n                        </children>\n\n                        <gap>10</gap>\n                    </flow-layout>\n                </children>\n\n                <horizontal-alignment>center</horizontal-alignment>\n\n                <gap>5</gap>\n                <padding>\n                    <all>5</all>\n                </padding>\n                <surface>\n                    <panel dark=\"true\"/>\n                </surface>\n            </flow-layout>\n        </template>\n\n    </templates>\n</owo-ui>\n\n"
  },
  {
    "path": "src/testmod/resources/assets/uwu/owo_ui/expand_gap_test.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"vertical\">\n            <children>\n                <!-- expand + gap, bug -->\n                <flow-layout direction=\"horizontal\">\n                    <children>\n                        <flow-layout direction=\"vertical\">\n                            <children>\n                                <!-- ... -->\n                            </children>\n\n                            <sizing>\n                                <vertical method=\"fixed\">20</vertical>\n                                <horizontal method=\"fixed\">20</horizontal>\n                            </sizing>\n\n                            <surface>\n                                <panel/>\n                            </surface>\n                        </flow-layout>\n\n                        <flow-layout direction=\"vertical\">\n                            <children>\n                                <!-- ... -->\n                            </children>\n\n                            <sizing>\n                                <vertical method=\"fixed\">20</vertical>\n                                <horizontal method=\"expand\">100</horizontal>\n                            </sizing>\n\n                            <surface>\n                                <panel/>\n                            </surface>\n                        </flow-layout>\n                    </children>\n\n                    <gap>10</gap>\n                </flow-layout>\n\n                <!-- margins -->\n                <flow-layout direction=\"horizontal\">\n                    <children>\n                        <flow-layout direction=\"vertical\">\n                            <children>\n                                <!-- ... -->\n                            </children>\n\n                            <sizing>\n                                <vertical method=\"fixed\">20</vertical>\n                                <horizontal method=\"fixed\">20</horizontal>\n                            </sizing>\n\n                            <surface>\n                                <panel/>\n                            </surface>\n\n                            <margins>\n                                <right>10</right>\n                            </margins>\n                        </flow-layout>\n\n                        <flow-layout direction=\"vertical\">\n                            <children>\n                                <!-- ... -->\n                            </children>\n\n                            <sizing>\n                                <vertical method=\"fixed\">20</vertical>\n                                <horizontal method=\"expand\">100</horizontal>\n                            </sizing>\n\n                            <surface>\n                                <panel/>\n                            </surface>\n                        </flow-layout>\n                    </children>\n                </flow-layout>\n            </children>\n\n            <gap>5</gap>\n        </flow-layout>\n    </components>\n</owo-ui>"
  },
  {
    "path": "src/testmod/resources/assets/uwu/owo_ui/focus_cycle_test.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"horizontal\">\n            <children>\n                <grid-layout rows=\"2\" columns=\"3\">\n                    <children>\n                        <button row=\"0\" column=\"0\">\n                            <text>1</text>\n                        </button>\n                        <button row=\"0\" column=\"1\">\n                            <text>2</text>\n                        </button>\n                        <button row=\"0\" column=\"2\">\n                            <text>3</text>\n                        </button>\n                        <button row=\"1\" column=\"0\">\n                            <text>4</text>\n                        </button>\n                        <button row=\"1\" column=\"1\">\n                            <text>5</text>\n                        </button>\n                        <button row=\"1\" column=\"2\">\n                            <text>6</text>\n                        </button>\n                    </children>\n                </grid-layout>\n\n                <stack-layout>\n                    <children>\n                        <button>\n                            <sizing>\n                                <horizontal method=\"fixed\">100</horizontal>\n                                <vertical method=\"fixed\">20</vertical>\n                            </sizing>\n                        </button>\n\n                        <button>\n                            <sizing>\n                                <horizontal method=\"fixed\">20</horizontal>\n                                <vertical method=\"fixed\">100</vertical>\n                            </sizing>\n                        </button>\n                    </children>\n\n                    <horizontal-alignment>center</horizontal-alignment>\n                    <vertical-alignment>center</vertical-alignment>\n                </stack-layout>\n\n                <flow-layout direction=\"ltr-text-flow\">\n                    <children>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack></item>\n                        <item><stack>minecraft:diamond</stack>d</item>\n                    </children>\n\n                    <sizing>\n                        <horizontal method=\"fill\">40</horizontal>\n                    </sizing>\n                </flow-layout>\n\n            </children>\n\n            <horizontal-alignment>center</horizontal-alignment>\n            <vertical-alignment>center</vertical-alignment>\n\n            <surface>\n                <blur quality=\"3\" size=\"5\"/>\n            </surface>\n        </flow-layout>\n    </components>\n</owo-ui>"
  },
  {
    "path": "src/testmod/resources/assets/uwu/owo_ui/parse_fail.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"horizontal\">\n            <children>\n                <button>\n                    <sizing>\n                        <horizontal method=\"fixed\">100</horizontal>\n                        <vertical method=\"fixed\">20</vertical>\n                    </sizing>\n                </button>\n            </children>\n\n            <horizontal-alignment>center</horizontal-alignment>\n            <vertical-alignment>center</vertical-alignment>\n\n            <surface>\n                <blur quality=\"3\" size=\"5\"/>\n            </surface>\n            <!-- invalid xml -->\n        </flow-layout\n    </components>\n</owo-ui>"
  },
  {
    "path": "src/testmod/resources/assets/uwu/owo_ui/smol_components.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <components>\n        <flow-layout direction=\"horizontal\">\n            <children>\n                <flow-layout direction=\"vertical\">\n                    <children>\n                        <small-checkbox/>\n\n                        <small-checkbox>\n                            <label>bruh</label>\n                            <checked>true</checked>\n                        </small-checkbox>\n\n                        <small-checkbox>\n                            <label>shadownite</label>\n                            <label-shadow>true</label-shadow>\n                        </small-checkbox>\n\n                        <slim-slider direction=\"horizontal\" id=\"precise-slider\">\n                            <sizing>\n                                <horizontal method=\"fixed\">100</horizontal>\n                            </sizing>\n                        </slim-slider>\n\n                        <slim-slider direction=\"horizontal\" id=\"tiny-steppy-man\">\n                            <sizing>\n                                <horizontal method=\"fixed\">100</horizontal>\n                            </sizing>\n\n                            <min>1.5</min>\n                            <max>3</max>\n                            <step-size>0.1</step-size>\n                            <value>2.6</value>\n                        </slim-slider>\n\n                        <flow-layout direction=\"horizontal\">\n                            <children>\n                                <slim-slider direction=\"vertical\" id=\"big-steppy-man\">\n                                    <sizing>\n                                        <vertical method=\"fixed\">100</vertical>\n                                    </sizing>\n\n                                    <min>20</min>\n                                    <max>80</max>\n                                    <step-size>10</step-size>\n                                </slim-slider>\n\n                                <flow-layout direction=\"horizontal\">\n                                    <children>\n                                        <label>\n                                            <text>This is a very epic inset moment, we can really *feel* the social interactions energy</text>\n                                            <max-width>80</max-width>\n                                        </label>\n                                    </children>\n\n                                    <padding>\n                                        <all>4</all>\n                                    </padding>\n\n                                    <surface>\n                                        <panel-inset/>\n                                    </surface>\n                                </flow-layout>\n                            </children>\n\n                            <gap>10</gap>\n                        </flow-layout>\n                    </children>\n\n                    <padding>\n                        <all>5</all>\n                    </padding>\n\n                    <gap>3</gap>\n\n                    <surface>\n                        <panel/>\n                    </surface>\n                </flow-layout>\n\n                <flow-layout direction=\"vertical\">\n                    <children>\n                        <template name=\"inset-panel\">\n                            <border-size>4</border-size>\n                        </template>\n\n                        <template name=\"inset-panel\">\n                            <border-size>6</border-size>\n                        </template>\n\n                        <template name=\"inset-panel\">\n                            <border-size>8</border-size>\n                        </template>\n\n                        <flow-layout direction=\"horizontal\">\n                            <children>\n                                <box>\n                                    <color>white</color>\n                                    <fill>true</fill>\n\n                                    <positioning type=\"absolute\">0,0</positioning>\n\n                                    <sizing>\n                                        <horizontal method=\"fixed\">128</horizontal>\n                                        <vertical method=\"fixed\">64</vertical>\n                                    </sizing>\n                                </box>\n\n                                <flow-layout direction=\"vertical\">\n                                    <children>\n                                        <item>\n                                            <stack>minecraft:diamond</stack>\n                                            <sizing>\n                                                <horizontal method=\"fixed\">8</horizontal>\n                                                <vertical method=\"fixed\">8</vertical>\n                                            </sizing>\n                                        </item>\n\n                                        <item>\n                                            <stack>minecraft:diamond</stack>\n                                        </item>\n\n                                        <item>\n                                            <stack>minecraft:diamond</stack>\n                                            <sizing>\n                                                <horizontal method=\"fixed\">32</horizontal>\n                                                <vertical method=\"fixed\">32</vertical>\n                                            </sizing>\n                                        </item>\n                                    </children>\n\n                                    <horizontal-alignment>center</horizontal-alignment>\n                                </flow-layout>\n\n                                <flow-layout direction=\"vertical\">\n                                    <children>\n                                        <item>\n                                            <stack>minecraft:stone</stack>\n                                            <sizing>\n                                                <horizontal method=\"fixed\">8</horizontal>\n                                                <vertical method=\"fixed\">8</vertical>\n                                            </sizing>\n                                        </item>\n\n                                        <item>\n                                            <stack>minecraft:stone</stack>\n                                        </item>\n\n                                        <item>\n                                            <stack>minecraft:stone</stack>\n                                            <sizing>\n                                                <horizontal method=\"fixed\">32</horizontal>\n                                                <vertical method=\"fixed\">32</vertical>\n                                            </sizing>\n                                        </item>\n                                    </children>\n\n                                    <horizontal-alignment>center</horizontal-alignment>\n                                </flow-layout>\n                            </children>\n\n                            <gap>5</gap>\n                            <horizontal-alignment>center</horizontal-alignment>\n                        </flow-layout>\n\n<!--                        <flow-layout direction=\"ltr-text-flow\">-->\n<!--                            <children>-->\n<!--                                <label>-->\n<!--                                    <text>1</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>22</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>333</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>4444</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>55555</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>666666</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>77777777</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>88888888</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>999999999</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>101010101010101010</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n<!--                                <label>-->\n<!--                                    <text>yes</text>-->\n<!--                                </label>-->\n<!--                                <item>-->\n<!--                                    <stack>minecraft:iron_ingot</stack>-->\n<!--                                </item>-->\n\n<!--                            </children>-->\n\n<!--                            <vertical-alignment>center</vertical-alignment>-->\n\n<!--                            <sizing>-->\n<!--                                <horizontal method=\"fixed\">100</horizontal>-->\n<!--                                <vertical method=\"fixed\">200</vertical>-->\n<!--                            </sizing>-->\n\n<!--                            <margins>-->\n<!--                                <bottom>150</bottom>-->\n<!--                            </margins>-->\n<!--                        </flow-layout>-->\n                    </children>\n\n                    <gap>10</gap>\n                    <horizontal-alignment>center</horizontal-alignment>\n                </flow-layout>\n\n                <flow-layout direction=\"vertical\">\n                    <children>\n                        <flow-layout direction=\"horizontal\" id=\"inset-container\">\n                            <children>\n                                <flow-layout direction=\"horizontal\">\n                                    <children>\n                                        <slim-slider direction=\"horizontal\" id=\"inset-slider\">\n                                            <min>3</min>\n                                            <max>20</max>\n                                            <step-size>1</step-size>\n\n                                            <sizing>\n                                                <horizontal method=\"fill\">85</horizontal>\n                                            </sizing>\n                                        </slim-slider>\n                                    </children>\n\n                                    <horizontal-alignment>center</horizontal-alignment>\n                                    <vertical-alignment>center</vertical-alignment>\n\n                                    <sizing>\n                                        <horizontal method=\"fill\">100</horizontal>\n                                        <vertical method=\"fill\">100</vertical>\n                                    </sizing>\n\n                                    <surface>\n                                        <panel-inset/>\n                                    </surface>\n                                </flow-layout>\n                            </children>\n\n                            <padding>\n                                <all>3</all>\n                            </padding>\n\n                            <sizing>\n                                <horizontal method=\"fill\">20</horizontal>\n                                <vertical method=\"fixed\">50</vertical>\n                            </sizing>\n\n                            <surface>\n                                <panel/>\n                            </surface>\n                        </flow-layout>\n\n                        <flow-layout direction=\"vertical\">\n                            <children>\n                                <flow-layout direction=\"horizontal\">\n                                    <children>\n                                        <box id=\"expando-box\">\n                                            <color>aqua</color>\n                                            <fill>true</fill>\n\n                                            <sizing>\n                                                <horizontal method=\"fixed\">20</horizontal>\n                                                <vertical method=\"fill\">100</vertical>\n                                            </sizing>\n                                        </box>\n\n                                        <box>\n                                            <color>red</color>\n                                            <fill>true</fill>\n\n                                            <sizing>\n                                                <horizontal method=\"expand\">50</horizontal>\n                                                <vertical method=\"fill\">100</vertical>\n                                            </sizing>\n                                        </box>\n\n                                        <box>\n                                            <color>dark-purple</color>\n                                            <fill>true</fill>\n\n                                            <sizing>\n                                                <horizontal method=\"expand\">50</horizontal>\n                                                <vertical method=\"fill\">100</vertical>\n                                            </sizing>\n                                        </box>\n                                    </children>\n\n                                    <horizontal-alignment>center</horizontal-alignment>\n\n                                    <surface>\n                                        <vanilla-translucent/>\n                                    </surface>\n\n                                    <sizing>\n                                        <horizontal method=\"fixed\">100</horizontal>\n                                        <vertical method=\"fixed\">15</vertical>\n                                    </sizing>\n                                </flow-layout>\n\n                                <slim-slider direction=\"horizontal\" id=\"expando-slider\">\n                                    <min>5</min>\n                                    <value>20</value>\n                                    <max>60</max>\n                                    <step-size>1</step-size>\n\n                                    <sizing>\n                                        <horizontal method=\"fixed\">80</horizontal>\n                                    </sizing>\n\n                                    <margins>\n                                        <all>5</all>\n                                    </margins>\n                                </slim-slider>\n                            </children>\n\n                            <horizontal-alignment>center</horizontal-alignment>\n\n                            <surface>\n                                <panel-with-inset>4</panel-with-inset>\n                            </surface>\n                            <padding>\n                                <all>5</all>\n                            </padding>\n\n                            <margins>\n                                <all>10</all>\n                            </margins>\n                        </flow-layout>\n                    </children>\n                </flow-layout>\n\n                <text-area>\n                    <text>bruh</text>\n                    <max-lines>10</max-lines>\n                    <max-length>100</max-length>\n                    <display-char-count>true</display-char-count>\n                    <sizing>\n                        <horizontal method=\"fixed\">100</horizontal>\n                    </sizing>\n                </text-area>\n            </children>\n\n            <gap>15</gap>\n\n            <surface>\n                <vanilla-translucent/>\n            </surface>\n\n            <vertical-alignment>center</vertical-alignment>\n            <horizontal-alignment>center</horizontal-alignment>\n        </flow-layout>\n    </components>\n\n    <templates>\n        <template name=\"inset-panel\">\n            <flow-layout direction=\"horizontal\">\n                <children/>\n\n                <sizing>\n                    <horizontal method=\"fill\">20</horizontal>\n                    <vertical method=\"fixed\">50</vertical>\n                </sizing>\n\n                <surface>\n                    <panel-with-inset>{{border-size}}</panel-with-inset>\n                </surface>\n            </flow-layout>\n        </template>\n    </templates>\n</owo-ui>"
  },
  {
    "path": "src/testmod/resources/assets/uwu/owo_ui/test_element_one.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <templates>\n        <template name=\"bruh-button\">\n            <button>\n                <text>bruh</text>\n            </button>\n        </template>\n    </templates>\n</owo-ui>"
  },
  {
    "path": "src/testmod/resources/assets/uwu/owo_ui/test_element_two.xml",
    "content": "<owo-ui xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"../../../../../../owo-ui.xsd\">\n    <templates>\n        <template name=\"hud-element\">\n            <flow-layout direction=\"vertical\">\n                <children>\n                    <label>\n                        <text>hud momente</text>\n                    </label>\n\n                    <template name=\"bruh-button@uwu:test_element_one\"/>\n                </children>\n\n                <padding>\n                    <all>10</all>\n                </padding>\n\n                <surface>\n                    <blur size=\"10\" quality=\"3\"/>\n                    <outline>#77AA00FF</outline>\n                </surface>\n\n                <positioning type=\"relative\">0,50</positioning>\n            </flow-layout>\n        </template>\n    </templates>\n</owo-ui>"
  },
  {
    "path": "src/testmod/resources/assets/uwu/textures/gui/bikeshed.png.mcmeta",
    "content": "{\n  \"texture\": {\n    \"blur\": true\n  }\n}"
  },
  {
    "path": "src/testmod/resources/data/uwu/item_group_tabs/crab_group.json5",
    "content": "{\n  target_group: \"uwu:vanilla_group\",\n  tabs: [\n    {\n      tag: \"minecraft:diamond_ores\",\n      name: \"tab_0\",\n      icon: \"minecraft:deepslate\"\n    },\n    {\n      tag: \"minecraft:gold_ores\",\n      name: \"tab_1\",\n      icon: \"minecraft:emerald\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src/testmod/resources/data/uwu/item_group_tabs/food_and_drink_button.json5",
    "content": "{\n  target_group: \"minecraft:food_and_drinks\",\n  extend: true,\n  buttons: [\n    {\n      name: \"consume\",\n      link: \"https://www.powerthesaurus.org/consume/synonyms\",\n      texture: \"minecraft:textures/block/sculk_catalyst_top.png\",\n      texture_u: 0,\n      texture_v: 0,\n      texture_width: 16,\n      texture_height: 16\n    }\n  ]\n}\n"
  },
  {
    "path": "src/testmod/resources/data/uwu/item_group_tabs/ingredients_extension.json5",
    "content": "{\n  target_group: \"minecraft:ingredients\",\n  extend: true,\n  tabs: [\n    {\n      tag: \"minecraft:diamond_ores\",\n      name: \"extra_ores\",\n      icon: \"minecraft:deepslate\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src/testmod/resources/data/uwu/item_group_tabs/ingredients_extension_2.json5",
    "content": "{\n  target_group: \"minecraft:ingredients\",\n  extend: true,\n  tabs: [\n    {\n      tag: \"minecraft:logs\",\n      name: \"the_logs\",\n      icon: \"minecraft:stripped_oak_log\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src/testmod/resources/data/uwu/recipe/test_recipe.json5",
    "content": "{\n  type: \"minecraft:crafting_shaped\",\n  key: {\n    O : \"minecraft:stick\",\n    X: \"minecraft:sand\"\n  },\n  \"owo:remainders\": {\n    \"minecraft:sand\": \"minecraft:sand\",\n    \"minecraft:stick\": {\n      id: \"minecraft:stick\",\n      count: 64\n    }\n  },\n  pattern: [\n    \"XXX\",\n    \"XOX\",\n    \"XXX\"\n  ],\n  result: {\n    id: \"minecraft:bedrock\"\n  },\n  // no way a comment?!?!?!?\n}\n"
  },
  {
    "path": "src/testmod/resources/data/uwu/recipe/uwu_shaped_recipe.json5",
    "content": "{\n  type: \"uwu:crafting_shaped\",\n  key: {\n    O: {\n      item: \"minecraft:cobblestone\"\n    },\n    X: {\n      item: \"minecraft:sand\"\n    }\n  },\n  pattern: [\n    \"XXX\",\n    \"XOX\",\n    \"XXX\"\n  ],\n  result: {\n    item: \"minecraft:stone\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/data/uwu/recipe/what_the_bucket_doin.json5",
    "content": "{\n  type: \"minecraft:crafting_shapeless\",\n  \"owo:remainders\": {\n    \"minecraft:water_bucket\": \"minecraft:potato\",\n  },\n  ingredients: [\n    \"minecraft:water_bucket\"\n  ],\n  result: {\n    id: \"minecraft:water_bucket\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/data/uwu/tags/item/tab_2_content.json5",
    "content": "{\n  values: [\n    \"minecraft:cobblestone\",\n    \"minecraft:deepslate\"\n  ]\n}\n"
  },
  {
    "path": "src/testmod/resources/fabric.mod.json",
    "content": "{\n  \"schemaVersion\": 1,\n  \"id\": \"uwu\",\n  \"version\": \"${version}\",\n  \"name\": \"uωu\",\n  \"description\": \"oωo testmod\",\n  \"authors\": [\n    \"glisco\"\n  ],\n  \"contributors\": [\n    \"chyzman\"\n  ],\n  \"contact\": {},\n  \"license\": \"MIT\",\n  \"environment\": \"*\",\n  \"mixins\": [\n    \"uwu.mixins.json\"\n  ],\n  \"entrypoints\": {\n    \"main\": [\n      \"io.wispforest.uwu.Uwu\"\n    ],\n    \"client\": [\n      \"io.wispforest.uwu.client.UwuClient\"\n    ],\n    \"rei_client\": [\n      \"io.wispforest.uwu.rei.UwuReiPlugin\"\n    ]\n  },\n  \"depends\": {\n    \"fabricloader\": \"*\",\n    \"fabric\": \"*\",\n    \"minecraft\": \">=1.18-alpha.21.40.a\",\n    \"owo\": \"*\"\n  }\n}\n"
  },
  {
    "path": "src/testmod/resources/owo-json5",
    "content": ""
  },
  {
    "path": "src/testmod/resources/uwu.mixins.json",
    "content": "{\n  \"required\": true,\n  \"minVersion\": \"0.8\",\n  \"package\": \"io.wispforest.uwu.mixin\",\n  \"compatibilityLevel\": \"JAVA_16\",\n  \"mixins\": [],\n  \"client\": [\n    \"TitleScreenMixin\",\n    \"GlRenderPassMixin\"\n  ],\n  \"injectors\": {\n    \"defaultRequire\": 1\n  }\n}"
  },
  {
    "path": "stylesheet.css",
    "content": "/*\n * Javadoc style sheet\n */\n\n@import url('https://fonts.googleapis.com/css2?family=Roboto&family=Roboto+Mono&display=swap');\n\n/*\n * Styles for individual HTML elements.\n *\n * These are styles that are specific to individual HTML elements. Changing them affects the style of a particular\n * HTML element throughout the page.\n */\n\nbody {\n    background-color: #2E303E;\n    color: #D4D7F2;\n    font-family: 'Roboto', Arial, Helvetica, sans-serif;\n    font-size: 18px;\n    margin: 0;\n    padding: 0;\n    height: 100%;\n    width: 100%;\n}\n\niframe {\n    margin: 0;\n    padding: 0;\n    height: 100%;\n    width: 100%;\n    overflow-y: scroll;\n    border: none;\n}\n\na:link,\na:visited {\n    text-decoration: none;\n    color: #5281d9;\n}\n\na[href]:hover,\na[href]:focus {\n    text-decoration: none;\n    color: #6792e3;\n}\n\na[name] {\n    color: #353833;\n}\n\npre {\n    font-family: 'Roboto Mono', monospace;\n    font-size: 14px;\n}\n\nh1 {\n    font-size: 30px;\n}\n\nh2 {\n    font-size: 18px;\n}\n\nh3 {\n    font-size: 16px;\n}\n\nh4 {\n    font-size: 13px;\n}\n\nh5 {\n    font-size: 12px;\n}\n\nh6 {\n    font-size: 11px;\n}\n\nul {\n    list-style-type: disc;\n}\n\ndiv.block code,\ntt {\n    background-color: #21222c;\n}\n\ncode,\ntt {\n    font-family: 'Roboto Mono', monospace;\n    font-size: 14px;\n    padding: 3px;\n    margin-top: 8px;\n    line-height: 1.4em;\n    border-radius: 5px;\n}\n\ndt code {\n    font-family: 'Roboto Mono', monospace;\n    font-size: 14px;\n    padding-top: 4px;\n}\n\n.summary-table dt code {\n    font-family: 'Roboto Mono', monospace;\n    font-size: 14px;\n    vertical-align: top;\n    padding-top: 4px;\n}\n\nsup {\n    font-size: 8px;\n}\n\nbutton {\n    font-family: 'Roboto', Arial, Helvetica, sans-serif;\n    font-size: 14px;\n}\n\n\n/*\n * Styles for HTML generated by javadoc.\n *\n * These are style classes that are used by the standard doclet to generate HTML documentation.\n */\n\n\n/*\n * Styles for document title and copyright.\n */\n\n.clear {\n    clear: both;\n    height: 0;\n    overflow: hidden;\n}\n\n.about-language {\n    float: right;\n    padding: 0 21px 8px 8px;\n    font-size: 11px;\n    margin-top: -9px;\n    height: 2.9em;\n}\n\n.legal-copy {\n    margin-left: .5em;\n}\n\n.tab {\n    background-color: #0066FF;\n    color: #ffffff;\n    padding: 8px;\n    width: 5em;\n    font-weight: bold;\n}\n\n\n/*\n * Styles for navigation bar.\n */\n\n@media screen {\n    .flex-box {\n        position: fixed;\n        display: flex;\n        flex-direction: column;\n        height: 100%;\n        width: 100%;\n    }\n    .flex-header {\n        flex: 0 0 auto;\n    }\n    .flex-content {\n        flex: 1 1 auto;\n        overflow-y: auto;\n    }\n}\n\n.top-nav {\n    background-color: #34439b;\n    color: #FFFFFF;\n    float: left;\n    padding: 0;\n    width: 100%;\n    clear: right;\n    min-height: 2.8em;\n    padding-top: 10px;\n    overflow: hidden;\n    font-size: 12px;\n}\n\n.sub-nav {\n    background-color: #4051b5;\n    float: left;\n    width: 100%;\n    overflow: hidden;\n    font-size: 12px;\n}\n\n.sub-nav div {\n    clear: left;\n    float: left;\n    padding: 0 0 5px 6px;\n    text-transform: uppercase;\n}\n\n.sub-nav .nav-list {\n    padding-top: 5px;\n}\n\nul.nav-list {\n    display: block;\n    margin: 0 25px 0 0;\n    padding: 0;\n}\n\nul.sub-nav-list {\n    float: left;\n    margin: 0 25px 0 0;\n    padding: 0;\n    display: none;\n}\n\nul.nav-list li {\n    list-style: none;\n    float: left;\n    padding: 5px 6px;\n    text-transform: uppercase;\n}\n\n.sub-nav .nav-list-search {\n    float: right;\n    margin: 0 0 0 0;\n    padding: 5px 6px;\n    clear: none;\n}\n\n.nav-list-search label {\n    position: relative;\n    right: -16px;\n}\n\nul.sub-nav-list li {\n    list-style: none;\n    float: left;\n    padding-top: 10px;\n}\n\n.top-nav a:link,\n.top-nav a:active,\n.top-nav a:visited {\n    color: #FFFFFF;\n    text-decoration: none;\n    text-transform: uppercase;\n}\n\n.top-nav a:hover {\n    text-decoration: none;\n    text-transform: uppercase;\n}\n\n.nav-bar-cell1-rev {\n    background-color: #2F3C86;\n    margin: auto 5px;\n}\n\n.skip-nav {\n    position: absolute;\n    top: auto;\n    left: -9999px;\n    overflow: hidden;\n}\n\n\n/*\n * Hide navigation links and search box in print layout\n */\n\n@media print {\n    ul.nav-list,\n    div.sub-nav {\n        display: none;\n    }\n}\n\n\n/*\n * Styles for page header and footer.\n */\n\n.title {\n    color: white;\n}\n\nhr {\n    border-color: #9497B2;\n    border-radius: 2px;\n    border-style: solid;\n}\n\n.sub-title {\n    margin: 5px 0 0 0;\n}\n\n.header ul {\n    margin: 0 0 15px 0;\n    padding: 0;\n}\n\n.header ul li,\n.footer ul li {\n    list-style: none;\n    font-size: 13px;\n}\n\n\n/*\n * Styles for headings.\n */\n\nbody.class-declaration-page .summary h2,\nbody.class-declaration-page .details h2,\nbody.class-use-page h2,\nbody.module-declaration-page .block-list h2 {\n    padding: 0;\n    margin: 15px 0;\n}\n\nbody.class-declaration-page .summary h3,\nbody.class-declaration-page .details h3,\nbody.class-declaration-page .summary .inherited-list h2 {\n    background-color: #292936;\n    margin: 0 0 6px -8px;\n    padding: 7px 5px;\n    border-radius: 5px;\n}\n\n\n/*\n * Styles for page layout containers.\n */\n\nmain {\n    clear: both;\n    padding: 30px 110px;\n    position: relative;\n    background-color: #2E303E;\n}\n\ndl.notes>dt {\n    font-family: 'Roboto', Arial, Helvetica, sans-serif;\n    font-size: 14px;\n    font-weight: bold;\n    margin: 10px 0 0 0;\n    color: white;\n}\n\ndl.notes>dd {\n    margin: 5px 0 10px 0;\n    font-size: 14px;\n    font-family: 'Roboto', Georgia, \"Times New Roman\", Times, serif;\n}\n\ndl.name-value>dt {\n    margin-left: 1px;\n    font-size: 1.1em;\n    display: inline;\n    font-weight: bold;\n}\n\ndl.name-value>dd {\n    margin: 0 0 0 1px;\n    font-size: 1.1em;\n    display: inline;\n}\n\n\n/*\n * Styles for lists.\n */\n\nli.circle {\n    list-style: circle;\n}\n\nul.horizontal li {\n    display: inline;\n    font-size: 0.9em;\n}\n\ndiv.inheritance {\n    margin: 0;\n    padding: 0;\n    padding-bottom: 5px;\n}\n\ndiv.inheritance div.inheritance {\n    margin-left: 2em;\n}\n\nul.block-list,\nul.details-list,\nul.member-list,\nul.summary-list {\n    margin: 10px 0 10px 0;\n    padding: 0;\n}\n\nul.block-list>li,\nul.details-list>li,\nul.member-list>li,\nul.summary-list>li {\n    list-style: none;\n    margin-bottom: 15px;\n    line-height: 1.4;\n}\n\n.summary-table dl,\n.summary-table dl dt,\n.summary-table dl dd {\n    margin-top: 0;\n    margin-bottom: 1px;\n}\n\n\n/*\n * Styles for tables.\n */\n\n.summary-table {\n    width: 100%;\n    border-spacing: 0;\n}\n\n.summary-table {\n    padding: 0;\n}\n\n.caption {\n    position: relative;\n    text-align: left;\n    background-repeat: no-repeat;\n    color: #253441;\n    font-weight: bold;\n    clear: none;\n    overflow: hidden;\n    padding: 0px;\n    padding-top: 10px;\n    padding-left: 1px;\n    margin: 0px;\n    white-space: pre;\n}\n\n.caption a:link,\n.caption a:visited {\n    color: #1f389c;\n}\n\n.caption a:hover,\n.caption a:active {\n    color: #FFFFFF;\n}\n\n.caption span {\n    white-space: nowrap;\n    padding-top: 5px;\n    padding-left: 12px;\n    padding-right: 12px;\n    padding-bottom: 7px;\n    display: inline-block;\n    float: left;\n    background-color: #4051B5;\n    color: white;\n    border: none;\n    height: 16px;\n    border-radius: 6px;\n    font-size: 14px;\n    margin-bottom: 10px;\n}\n\ndiv.table-tabs>button {\n    border: none;\n    cursor: pointer;\n    padding: 5px 12px 7px 12px;\n    font-weight: bold;\n    margin-right: 6px;\n    border-radius: 6px;\n}\n\ndiv.table-tabs>button.active-table-tab {\n    background: #2F3C86;\n    color: white;\n}\n\ndiv.table-tabs>button.table-tab {\n    background: #4051b5;\n    color: #FFFFFF;\n}\n\n.two-column-summary {\n    display: grid;\n    grid-template-columns: minmax(15%, max-content) minmax(15%, auto);\n}\n\n.three-column-summary {\n    display: grid;\n    grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, auto);\n}\n\n.four-column-summary {\n    display: grid;\n    grid-template-columns: minmax(10%, max-content) minmax(10%, max-content) minmax(10%, max-content) minmax(10%, auto);\n}\n\n@media screen and (max-width: 600px) {\n    .two-column-summary {\n        display: grid;\n        grid-template-columns: 1fr;\n    }\n}\n\n@media screen and (max-width: 800px) {\n    .three-column-summary {\n        display: grid;\n        grid-template-columns: minmax(10%, max-content) minmax(25%, auto);\n    }\n    .three-column-summary .col-last {\n        grid-column-end: span 2;\n    }\n}\n\n@media screen and (max-width: 1000px) {\n    .four-column-summary {\n        display: grid;\n        grid-template-columns: minmax(15%, max-content) minmax(15%, auto);\n    }\n}\n\n.summary-table>div {\n    text-align: left;\n    padding: 10px 3px 8px 7px;\n}\n\n.col-first,\n.col-second,\n.col-last,\n.col-constructor-name,\n.col-deprecated-item-name {\n    vertical-align: top;\n    padding-right: 0;\n    padding-top: 8px;\n    padding-bottom: 3px;\n}\n\n.table-header {\n    background: #21222C;\n    font-weight: bold;\n}\n\n.table-header.col-first {\n    border-top-left-radius: 10px;\n}\n\n.table-header.col-last {\n    border-top-right-radius: 10px;\n}\n\n.col-last:last-child {\n    border-bottom-right-radius: 10px;\n}\n\n.two-column-summary .col-first:nth-last-child(2) {\n    border-bottom-left-radius: 10px;\n}\n\n.two-column-summary>.col-constructor-name:nth-last-child(2),\n.col-first:nth-last-child(3) {\n    border-bottom-left-radius: 10px;\n}\n\n@media screen and (max-width: 800px) {\n\n    .table-header.col-first {\n        border-top-right-radius: 10px;\n        border-top-left-radius: 10px;\n    }\n\n    .table-header.col-last {\n        border-top-right-radius: 0px;\n    }\n\n    .col-last:last-child {\n        border-bottom-left-radius: 10px;\n        border-bottom-right-radius: 10px;\n    }\n\n    .two-column-summary .col-first:nth-last-child(2) {\n        border-bottom-left-radius: 0px;\n    }\n\n    .two-column-summary>.col-constructor-name:nth-last-child(2),\n    .col-first:nth-last-child(3) {\n        border-bottom-left-radius: 0px;\n    }\n}\n\n.col-first,\n.col-first {\n    font-size: 13px;\n}\n\n.col-second,\n.col-second,\n.col-last,\n.col-constructor-name,\n.col-deprecated-item-name,\n.col-last {\n    font-size: 13px;\n}\n\n.col-first,\n.col-second,\n.col-constructor-name {\n    vertical-align: top;\n    overflow: auto;\n}\n\n.col-last {\n    white-space: normal;\n}\n\n.col-first a:link,\n.col-first a:visited,\n.col-second a:link,\n.col-second a:visited,\n.col-first a:link,\n.col-first a:visited,\n.col-second a:link,\n.col-second a:visited,\n.col-constructor-name a:link,\n.col-constructor-name a:visited,\n.col-deprecated-item-name a:link,\n.col-deprecated-item-name a:visited,\n.constant-values-container a:link,\n.constant-values-container a:visited,\n.all-classes-container a:link,\n.all-classes-container a:visited,\n.all-packages-container a:link,\n.all-packages-container a:visited {\n    font-weight: bold;\n}\n\n.table-sub-heading-color {\n    background-color: #EEEEFF;\n}\n\n.even-row-color,\n.even-row-color .table-header {\n    background-color: #272834;\n}\n\n.odd-row-color,\n.odd-row-color .table-header {\n    background-color: #232430;\n}\n\n\n/*\n * Styles for contents.\n */\n\n.deprecated-content {\n    margin: 0;\n    padding: 10px 0;\n}\n\ndiv.block {\n    font-size: 16px;\n    font-family: 'Roboto', Georgia, \"Times New Roman\", Times, serif;\n    padding-bottom: 15px;\n    line-height: 1.85em;\n}\n\n.col-last div {\n    padding-top: 0;\n}\n\n.col-last a {\n    padding-bottom: 3px;\n}\n\n.module-signature,\n.package-signature,\n.type-signature,\n.member-signature {\n    font-family: 'Roboto Mono', monospace;\n    font-size: 14px;\n    margin: 14px 0;\n    white-space: pre-wrap;\n}\n\n.module-signature,\n.package-signature,\n.type-signature {\n    margin-top: 30px;\n}\n\n.member-signature .type-parameters-long,\n.member-signature .parameters,\n.member-signature .exceptions {\n    display: inline-block;\n    vertical-align: top;\n    white-space: pre;\n}\n\n.member-signature .type-parameters {\n    white-space: normal;\n}\n\n\n/*\n * Styles for formatting effect.\n */\n\n.source-line-no {\n    color: green;\n    padding: 0 30px 0 0;\n}\n\nh1.hidden {\n    visibility: hidden;\n    overflow: hidden;\n    font-size: 10px;\n}\n\n.block {\n    display: block;\n    margin: 0 10px 5px 0;\n    color: #D4D7F2;\n}\n\n.deprecated-label,\n.descfrm-type-label,\n.implementation-label,\n.member-name-label,\n.member-name-link,\n.module-label-in-package,\n.module-label-in-type,\n.override-specify-label,\n.package-label-in-type,\n.package-hierarchy-label,\n.type-name-label,\n.type-name-link,\n.search-tag-link {\n    font-weight: bold;\n}\n\n.deprecation-block {\n    font-size: 14px;\n    font-family: 'Roboto', Georgia, \"Times New Roman\", Times, serif;\n    border-style: solid;\n    border-width: thin;\n    border-radius: 10px;\n    padding: 10px;\n    margin-bottom: 10px;\n    margin-right: 10px;\n    display: inline-block;\n}\n\ndiv.block div.deprecation-comment,\ndiv.block div.block span.emphasized-phrase,\ndiv.block div.block span.interface-name {\n    font-style: normal;\n}\n\n\n/*\n * Styles specific to HTML5 elements.\n */\n\nmain,\nnav,\nheader,\nfooter,\nsection {\n    display: block;\n}\n\n\n/*\n * Styles for javadoc search.\n */\n\n.ui-autocomplete-category {\n    font-weight: bold;\n    font-size: 15px;\n    padding: 7px 0 7px 3px;\n    background-color: #4051b5;\n    color: #FFFFFF;\n}\n\n.result-item {\n    font-size: 13px;\n}\n\n.ui-autocomplete {\n    max-height: 85%;\n    max-width: 65%;\n    overflow-y: scroll;\n    overflow-x: scroll;\n    white-space: nowrap;\n    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);\n}\n\nul.ui-autocomplete {\n    position: fixed;\n    z-index: 999999;\n}\n\nul.ui-autocomplete li {\n    float: left;\n    clear: both;\n    width: 100%;\n}\n\n.result-highlight {\n    font-weight: bold;\n}\n\n#search {\n    background-image: url('resources/glass.png');\n    background-size: 13px;\n    background-repeat: no-repeat;\n    background-position: 5px 6px;\n    padding-left: 20px;\n    position: relative;\n    right: -18px;\n    width: 400px;\n    border-color: white;\n    padding-top: 5px;\n    padding-bottom: 5px;\n    border-radius: 5px;\n    border-style: none;\n}\n\n#reset {\n    background-color: rgb(255, 255, 255);\n    background-image: url('resources/x.png');\n    background-position: center;\n    background-repeat: no-repeat;\n    background-size: 12px;\n    border: 0 none;\n    width: 16px;\n    height: 16px;\n    position: relative;\n    left: -4px;\n    top: -4px;\n    font-size: 0px;\n}\n\n.watermark {\n    color: #545454;\n}\n\n.search-tag-desc-result {\n    font-size: 11px;\n}\n\n.search-tag-holder-result {\n    font-size: 12px;\n}\n\n.search-tag-result:target {\n    background-color: yellow;\n}\n\n.module-graph span {\n    display: none;\n    position: absolute;\n}\n\n.module-graph:hover span {\n    display: block;\n    margin: -100px 0 0 100px;\n    z-index: 1;\n}\n\n.inherited-list {\n    margin: 10px 0 10px 0;\n}\n\nsection.description {\n    line-height: 1.4;\n}\n\n.summary section[class$=\"-summary\"],\n.details section[class$=\"-details\"],\n.class-uses .detail,\n.serialized-class-details {\n    padding: 0px 20px 5px 10px;\n    border: 1px solid #21222C;\n    background-color: #21222C;\n    border-radius: 6px;\n    box-shadow: 0 0 4px 1px rgb(0 0 0 / 25%);\n    box-sizing: border-box;\n    padding-bottom: 15px;\n}\n\n.inherited-list,\nsection[class$=\"-details\"] .detail {\n    padding: 0 0 5px 8px;\n    background-color: #21222C;\n    border: none;\n}\n\n.vertical-separator {\n    padding: 0 5px;\n}\n\nul.help-section-list {\n    margin: 0;\n}\n\n\n/*\n * Indicator icon for external links.\n */\n\nmain a[href*=\"://\"]::after {\n    content: \"\";\n    display: inline-block;\n    background-image: url('data:image/svg+xml; utf8, \\\n      <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"768\" height=\"768\">\\\n        <path d=\"M584 664H104V184h216V80H0v688h688V448H584zM384 0l132 \\\n        132-240 240 120 120 240-240 132 132V0z\" fill=\"%234a6782\"/>\\\n      </svg>');\n    background-size: 100% 100%;\n    width: 7px;\n    height: 7px;\n    margin-left: 2px;\n    margin-bottom: 4px;\n}\n\nmain a[href*=\"://\"]:hover::after,\nmain a[href*=\"://\"]:focus::after {\n    background-image: url('data:image/svg+xml; utf8, \\\n      <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"768\" height=\"768\">\\\n        <path d=\"M584 664H104V184h216V80H0v688h688V448H584zM384 0l132 \\\n        132-240 240 120 120 240-240 132 132V0z\" fill=\"%23bb7a2a\"/>\\\n      </svg>');\n}\n\n\n/*\n * Styles for user-provided tables.\n *\n * borderless:\n *      No borders, vertical margins, styled caption.\n *      This style is provided for use with existing doc comments.\n *      In general, borderless tables should not be used for layout purposes.\n *\n * plain:\n *      Plain borders around table and cells, vertical margins, styled caption.\n *      Best for small tables or for complex tables for tables with cells that span\n *      rows and columns, when the \"striped\" style does not work well.\n *\n * striped:\n *      Borders around the table and vertical borders between cells, striped rows,\n *      vertical margins, styled caption.\n *      Best for tables that have a header row, and a body containing a series of simple rows.\n */\n\ntable.borderless,\ntable.plain,\ntable.striped {\n    margin-top: 10px;\n    margin-bottom: 10px;\n}\n\ntable.borderless>caption,\ntable.plain>caption,\ntable.striped>caption {\n    font-weight: bold;\n    font-size: smaller;\n}\n\ntable.borderless th,\ntable.borderless td,\ntable.plain th,\ntable.plain td,\ntable.striped th,\ntable.striped td {\n    padding: 2px 5px;\n}\n\ntable.borderless,\ntable.borderless>thead>tr>th,\ntable.borderless>tbody>tr>th,\ntable.borderless>tr>th,\ntable.borderless>thead>tr>td,\ntable.borderless>tbody>tr>td,\ntable.borderless>tr>td {\n    border: none;\n}\n\ntable.borderless>thead>tr,\ntable.borderless>tbody>tr,\ntable.borderless>tr {\n    background-color: transparent;\n}\n\ntable.plain {\n    border-collapse: collapse;\n    border: 1px solid black;\n}\n\ntable.plain>thead>tr,\ntable.plain>tbody tr,\ntable.plain>tr {\n    background-color: transparent;\n}\n\ntable.plain>thead>tr>th,\ntable.plain>tbody>tr>th,\ntable.plain>tr>th,\ntable.plain>thead>tr>td,\ntable.plain>tbody>tr>td,\ntable.plain>tr>td {\n    border: 1px solid black;\n}\n\ntable.striped {\n    border-collapse: collapse;\n    border: 1px solid black;\n}\n\ntable.striped>thead {\n    background-color: #E3E3E3;\n}\n\ntable.striped>thead>tr>th,\ntable.striped>thead>tr>td {\n    border: 1px solid black;\n}\n\ntable.striped>tbody>tr:nth-child(even) {\n    background-color: #EEE\n}\n\ntable.striped>tbody>tr:nth-child(odd) {\n    background-color: #FFF\n}\n\ntable.striped>tbody>tr>th,\ntable.striped>tbody>tr>td {\n    border-left: 1px solid black;\n    border-right: 1px solid black;\n}\n\ntable.striped>tbody>tr>th {\n    font-weight: normal;\n}\n\n\n/**\n * Tweak font sizes and paddings for small screens.\n */\n\n@media screen and (max-width: 1050px) {\n    #search {\n        width: 300px;\n    }\n}\n\n@media screen and (max-width: 800px) {\n    #search {\n        width: 200px;\n    }\n    .top-nav,\n    .bottom-nav {\n        font-size: 11px;\n        padding-top: 6px;\n    }\n    .sub-nav {\n        font-size: 11px;\n    }\n    .about-language {\n        padding-right: 16px;\n    }\n    ul.nav-list li,\n    .sub-nav .nav-list-search {\n        padding: 6px;\n    }\n    ul.sub-nav-list li {\n        padding-top: 5px;\n    }\n    main {\n        padding: 10px;\n    }\n    .summary section[class$=\"-summary\"],\n    .details section[class$=\"-details\"],\n    .class-uses .detail,\n    .serialized-class-details {\n        padding: 0 8px 5px 8px;\n    }\n    body {\n        -webkit-text-size-adjust: none;\n    }\n}\n\n@media screen and (max-width: 500px) {\n    #search {\n        width: 150px;\n    }\n    .top-nav,\n    .bottom-nav {\n        font-size: 10px;\n    }\n    .sub-nav {\n        font-size: 10px;\n    }\n    .about-language {\n        font-size: 10px;\n        padding-right: 12px;\n    }\n}\n"
  }
]