Unknown vor 6 Jahren
Ursprung
Commit
1150ee90e3
57 geänderte Dateien mit 4507 neuen und 1 gelöschten Zeilen
  1. 27 0
      .gitignore
  2. 2 0
      .travis.yml
  3. 24 0
      LICENSE
  4. 67 1
      README.md
  5. 32 0
      build.gradle
  6. 6 0
      gradle.properties
  7. BIN
      gradle/wrapper/gradle-wrapper.jar
  8. 6 0
      gradle/wrapper/gradle-wrapper.properties
  9. 169 0
      gradlew
  10. 84 0
      gradlew.bat
  11. 20 0
      settings.gradle
  12. 71 0
      src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java
  13. 40 0
      src/main/java/me/shedaniel/clothconfig2/api/AbstractConfigEntry.java
  14. 23 0
      src/main/java/me/shedaniel/clothconfig2/api/AbstractConfigListEntry.java
  15. 74 0
      src/main/java/me/shedaniel/clothconfig2/api/ConfigBuilder.java
  16. 16 0
      src/main/java/me/shedaniel/clothconfig2/api/ConfigCategory.java
  17. 41 0
      src/main/java/me/shedaniel/clothconfig2/api/ConfigEntryBuilder.java
  18. 43 0
      src/main/java/me/shedaniel/clothconfig2/api/QueuedTooltip.java
  19. 454 0
      src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java
  20. 39 0
      src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigTabButton.java
  21. 97 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/BooleanListEntry.java
  22. 106 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/DoubleListEntry.java
  23. 118 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/EnumListEntry.java
  24. 106 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/FloatListEntry.java
  25. 106 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/IntegerListEntry.java
  26. 159 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/IntegerSliderEntry.java
  27. 106 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/LongListEntry.java
  28. 160 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/LongSliderEntry.java
  29. 41 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/StringListEntry.java
  30. 132 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/SubCategoryListEntry.java
  31. 106 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/TextFieldListEntry.java
  32. 73 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/TextListEntry.java
  33. 46 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/TooltipListEntry.java
  34. 53 0
      src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicElementListWidget.java
  35. 520 0
      src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicEntryListWidget.java
  36. 168 0
      src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicSmoothScrollingEntryListWidget.java
  37. 200 0
      src/main/java/me/shedaniel/clothconfig2/impl/ConfigBuilderImpl.java
  38. 40 0
      src/main/java/me/shedaniel/clothconfig2/impl/ConfigCategoryImpl.java
  39. 94 0
      src/main/java/me/shedaniel/clothconfig2/impl/ConfigEntryBuilderImpl.java
  40. 55 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/BooleanToggleBuilder.java
  41. 67 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/DoubleFieldBuilder.java
  42. 54 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/EnumSelectorBuilder.java
  43. 31 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/FieldBuilder.java
  44. 67 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/FloatFieldBuilder.java
  45. 67 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/IntFieldBuilder.java
  46. 62 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/IntSliderBuilder.java
  47. 67 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/LongFieldBuilder.java
  48. 52 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/LongSliderBuilder.java
  49. 153 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/SubCategoryBuilder.java
  50. 35 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/TextDescriptionBuilder.java
  51. 43 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/TextFieldBuilder.java
  52. 19 0
      src/main/resources/assets/cloth-config2/lang/en_us.json
  53. 18 0
      src/main/resources/assets/cloth-config2/lang/fr_ca.json
  54. 18 0
      src/main/resources/assets/cloth-config2/lang/fr_fr.json
  55. BIN
      src/main/resources/assets/cloth-config2/textures/gui/cloth_config.png
  56. 30 0
      src/main/resources/fabric.mod.json
  57. BIN
      src/main/resources/icon.png

+ 27 - 0
.gitignore

@@ -0,0 +1,27 @@
+# Compiled nonsense that does not belong in *source* control
+/build
+/bin
+/.gradle
+/minecraft
+/out
+/run
+/classes
+/old
+
+# IDE nonsense that could go in source control but really shouldn't
+.classpath
+.project
+.metadata
+.settings
+*.launch
+*.iml
+.idea
+*.ipr
+*.iws
+
+# Sekrit files
+private.properties
+
+# Files from bad operating systems :^)
+Thumbs.db
+.DS_Store

+ 2 - 0
.travis.yml

@@ -0,0 +1,2 @@
+language: java
+script: ./gradlew clean build

+ 24 - 0
LICENSE

@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org>

+ 67 - 1
README.md

@@ -1 +1,67 @@
-ClothConfig
+# Cloth Config
+## Maven
+```groovy
+repositories {
+    maven { url "https://minecraft.curseforge.com/api/maven"}
+}
+dependencies {
+    mmodCompile "cloth-config:ClothConfig:{RANDOMVERSION}"
+}
+```
+## APIs
+##### Config Screen v1 API
+Start by using `ConfigScreenBuilder.create`, inside it you can do `addCategory` to get the category instance. Do `addOption` with the category instance to add an option.
+```java
+ConfigScreenBuilder builder = ConfigScreenBuilder.create(parentScreen, screenTitleKey, saveConsumer);
+builder.addCategory("text.category.key").addOption(option);
+```
+There are multiple builtin option types:
+- Boolean -> BooleanListEntry
+- String -> StringListEntry
+- Integer -> IntegerListEntry (Text Field), IntegerSliderEntry (Slider)
+- Long -> LongListEntry (Text Field), LongSliderEntry (Slider)
+- Float -> FloatListEntry
+- Double -> DoubleListEntry
+- Enum -> EnumListEntry (Override enumNameProvider for custom names, or make the enum implement Translatable, or override `toString()` in the enum for names)
+- Text for Description -> TextListEntry
+
+And you can always build your own entry. Example of a boolean entry:
+```java
+builder.addCategory("text.category.key").addOption(new BooleanListEntry(fieldKey, value, save));
+```
+`fieldKey` will be translated automatically using `I18n`, `value` is the `true` or `false`, for `save`, it will only be called when you press save.
+
+Infect, you should do something like this:
+```java
+AtomicBoolean configBool = new AtomicBoolean(false);
+builder.addCategory("text.category.key").addOption(new BooleanListEntry("text.value.key", configBool, bool -> configBool.set(bool)));
+builder.setOnSave(savedConfig -> {
+    // Save your config data file here
+});
+```
+
+Lastly, you can open the screen like this:
+```java
+MinecraftClient.getInstance().openScreen(builder.build());
+```
+
+##### Config Screen v2 API
+Start by using `ConfigBuilder.create`, inside it you can do `getOrCreateCategory` to get the category instance. Do `addEntry` with the category instance to add an option.
+```java
+ConfigBuilder builder = ConfigBuilder.create().setParentScreen(parentScreen).setTitle(screenTitleKey).set(setSavingRunnable);
+builder.getOrCreateCategory("text.category.key").addEntry(option);
+```
+
+To start adding fields, do `ConfigEntryBuilder.create()` to get the entry builder instance.
+Example to add a boolean field:
+```java
+ConfigEntryBuilder entryBuilder = ConfigEntryBuilder.create();
+category.addEntry(entryBuilder.startBooleanToggle("path.to.your.key", false).buildEntry());
+```
+
+All builtin entry builders can be found in ConfigEntryBuilder.
+
+Lastly, you can open the screen like this:
+```java
+MinecraftClient.getInstance().openScreen(builder.build());
+```

+ 32 - 0
build.gradle

@@ -0,0 +1,32 @@
+plugins {
+    id 'fabric-loom' version '0.2.2-SNAPSHOT'
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+archivesBaseName = "ClothConfig"
+version = project.mod_version
+
+minecraft {
+}
+
+processResources {
+    filesMatching('fabric.mod.json') {
+        expand 'version': project.version
+    }
+    inputs.property "version", project.version
+}
+
+jar {
+    version = project.mod_version.replaceAll('\\+build.', '.')
+    classifier = null
+}
+
+dependencies {
+    minecraft "com.mojang:minecraft:${project.minecraft_version}"
+    mappings "net.fabricmc:yarn:${project.yarn_version}"
+    modCompile "net.fabricmc:fabric-loader:${project.fabric_loader_version}"
+    modCompile "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
+    modCompile "io.github.prospector.modmenu:ModMenu:${modmenu_version}"
+}

+ 6 - 0
gradle.properties

@@ -0,0 +1,6 @@
+minecraft_version=1.14.2
+yarn_version=1.14.2+build.2
+fabric_loader_version=0.4.7+build.147
+fabric_version=0.3.0-pre+build.165
+mod_version=0.1.0
+modmenu_version=1.5.4-85

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Sun Dec 30 21:50:18 HKT 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip

+ 169 - 0
gradlew

@@ -0,0 +1,169 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 20 - 0
settings.gradle

@@ -0,0 +1,20 @@
+pluginManagement {
+    repositories {
+        jcenter()
+        maven {
+            name = 'Fabric'
+            url = 'https://maven.fabricmc.net/'
+        }
+        maven {
+            name = "Forge"
+            url = "https://files.minecraftforge.net/maven/"
+        }
+        maven {
+            name = "Jitpack"
+            url = "https://jitpack.io/"
+        }
+        gradlePluginPortal()
+    }
+}
+
+rootProject.name = 'ClothConfig'

+ 71 - 0
src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java

@@ -0,0 +1,71 @@
+package me.shedaniel.clothconfig2;
+
+import com.google.common.collect.ImmutableList;
+import me.shedaniel.clothconfig2.api.ConfigBuilder;
+import me.shedaniel.clothconfig2.api.ConfigCategory;
+import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
+import me.shedaniel.clothconfig2.impl.builders.SubCategoryBuilder;
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.Identifier;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.Method;
+import java.util.Optional;
+
+public class ClothConfigInitializer implements ClientModInitializer {
+    
+    public static final Logger LOGGER = LogManager.getFormatterLogger("ClothConfig");
+    
+    @Override
+    public void onInitializeClient() {
+        if (FabricLoader.getInstance().isModLoaded("modmenu")) {
+            try {
+                Class<?> clazz = Class.forName("io.github.prospector.modmenu.api.ModMenuApi");
+                Method method = clazz.getMethod("addConfigOverride", String.class, Runnable.class);
+                method.invoke(null, "cloth-config2", (Runnable) () -> {
+                    ConfigBuilder builder = ConfigBuilder.create().setParentScreen(MinecraftClient.getInstance().currentScreen).setTitle("Cloth Mod Config Demo");
+                    builder.setDefaultBackgroundTexture(new Identifier("minecraft:textures/block/oak_planks.png"));
+                    ConfigCategory playZone = builder.getOrCreateCategory("Play Zone");
+                    ConfigEntryBuilder entryBuilder = ConfigEntryBuilder.create();
+                    playZone.addEntry(entryBuilder.startBooleanToggle("Simple Boolean", false).buildEntry());
+                    playZone.addEntry(entryBuilder.startTextField("Simple Boolean", "ab").setDefaultValue(() -> "ab").buildEntry());
+                    playZone.addEntry(entryBuilder.startLongSlider("Long Slider", 0, -10, 10).setDefaultValue(() -> 0l).buildEntry());
+                    playZone.addEntry(entryBuilder.startIntField("Integer Field", 2).setDefaultValue(() -> 2).setMin(2).setMax(99).buildEntry());
+                    SubCategoryBuilder randomCategory = entryBuilder.startSubCategory("Random Sub-Category");
+                    randomCategory.add(entryBuilder.startTextDescription("§7This is a promotional message brought to you by Danielshe. Shop your favorite Lil Tater at store.liltater.com!").setTooltipSupplier(() -> Optional.of(new String[]{"This is an example tooltip."})).buildEntry());
+                    randomCategory.add(entryBuilder.startSubCategory("Sub-Sub-Category", ImmutableList.of(entryBuilder.startEnumSelector("Enum Field No. 1", DemoEnum.class, DemoEnum.CONSTANT_2).setDefaultValue(() -> DemoEnum.CONSTANT_1).buildEntry(), entryBuilder.startEnumSelector("Enum Field No. 2", DemoEnum.class, DemoEnum.CONSTANT_2).setDefaultValue(() -> DemoEnum.CONSTANT_1).buildEntry())).buildEntry());
+                    for(int i = 0; i < 10; i++)
+                        randomCategory.add(entryBuilder.startIntSlider("Integer Slider No. " + (i + 1), 0, -99, 99).buildEntry());
+                    playZone.addEntry(randomCategory.buildEntry());
+                    ConfigCategory enumZone = builder.getOrCreateCategory("Enum Zone");
+                    enumZone.setCategoryBackground(new Identifier("minecraft:textures/block/stone.png"));
+                    enumZone.addEntry(entryBuilder.startEnumSelector("Enum Field", DemoEnum.class, DemoEnum.CONSTANT_2).setDefaultValue(() -> DemoEnum.CONSTANT_1).buildEntry());
+                    MinecraftClient.getInstance().openScreen(builder.build());
+                });
+            } catch (Exception e) {
+                ClothConfigInitializer.LOGGER.error("[ClothConfig] Failed to add test config override for ModMenu!", e);
+            }
+        }
+    }
+    
+    private static enum DemoEnum {
+        CONSTANT_1("Constant 1"),
+        CONSTANT_2("Constant 2"),
+        CONSTANT_3("Constant 3");
+        
+        private final String key;
+        
+        private DemoEnum(String key) {
+            this.key = key;
+        }
+        
+        @Override
+        public String toString() {
+            return this.key;
+        }
+    }
+    
+}

+ 40 - 0
src/main/java/me/shedaniel/clothconfig2/api/AbstractConfigEntry.java

@@ -0,0 +1,40 @@
+package me.shedaniel.clothconfig2.api;
+
+import me.shedaniel.clothconfig2.gui.ClothConfigScreen;
+import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
+
+import java.util.Optional;
+
+public abstract class AbstractConfigEntry<T> extends DynamicElementListWidget.ElementEntry<AbstractConfigEntry<T>> {
+    private ClothConfigScreen screen;
+    
+    public abstract String getFieldName();
+    
+    public abstract T getValue();
+    
+    public Optional<String> getError() {
+        return Optional.empty();
+    }
+    
+    public abstract Optional<T> getDefaultValue();
+    
+    public final ClothConfigScreen.ListWidget getParent() {
+        return screen.listWidget;
+    }
+    
+    public final ClothConfigScreen getScreen() {
+        return screen;
+    }
+    
+    @Deprecated
+    public final void setScreen(ClothConfigScreen screen) {
+        this.screen = screen;
+    }
+    
+    public abstract void save();
+    
+    @Override
+    public int getItemHeight() {
+        return 24;
+    }
+}

+ 23 - 0
src/main/java/me/shedaniel/clothconfig2/api/AbstractConfigListEntry.java

@@ -0,0 +1,23 @@
+package me.shedaniel.clothconfig2.api;
+
+public abstract class AbstractConfigListEntry<T> extends AbstractConfigEntry<T> {
+    private String fieldName;
+    private boolean editable = true;
+    
+    public AbstractConfigListEntry(String fieldName) {
+        this.fieldName = fieldName;
+    }
+    
+    public boolean isEditable() {
+        return getScreen().isEditable() && editable;
+    }
+    
+    public void setEditable(boolean editable) {
+        this.editable = editable;
+    }
+    
+    @Override
+    public String getFieldName() {
+        return fieldName;
+    }
+}

+ 74 - 0
src/main/java/me/shedaniel/clothconfig2/api/ConfigBuilder.java

@@ -0,0 +1,74 @@
+package me.shedaniel.clothconfig2.api;
+
+import me.shedaniel.clothconfig2.impl.ConfigBuilderImpl;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.util.Identifier;
+
+import java.util.function.Consumer;
+
+public interface ConfigBuilder {
+    
+    @SuppressWarnings("deprecation")
+    public static ConfigBuilder create() {
+        return new ConfigBuilderImpl();
+    }
+    
+    /**
+     * @deprecated Use {@link ConfigBuilder#create()}
+     */
+    @Deprecated
+    public static ConfigBuilder create(Screen parent, String title) {
+        return create().setParentScreen(parent).setTitle(title);
+    }
+    
+    Screen getParentScreen();
+    
+    ConfigBuilder setParentScreen(Screen parent);
+    
+    String getTitle();
+    
+    ConfigBuilder setTitle(String title);
+    
+    boolean isEditable();
+    
+    ConfigBuilder setEditable(boolean editable);
+    
+    ConfigCategory getOrCreateCategory(String categoryKey);
+    
+    ConfigBuilder removeCategory(String categoryKey);
+    
+    ConfigBuilder removeCategoryIfExists(String categoryKey);
+    
+    boolean hasCategory(String category);
+    
+    ConfigBuilder setShouldTabsSmoothScroll(boolean shouldTabsSmoothScroll);
+    
+    boolean isTabsSmoothScrolling();
+    
+    ConfigBuilder setShouldListSmoothScroll(boolean shouldListSmoothScroll);
+    
+    boolean isListSmoothScrolling();
+    
+    ConfigBuilder setDoesConfirmSave(boolean confirmSave);
+    
+    boolean doesConfirmSave();
+    
+    ConfigBuilder setDoesProcessErrors(boolean processErrors);
+    
+    boolean doesProcessErrors();
+    
+    Identifier getDefaultBackgroundTexture();
+    
+    ConfigBuilder setDefaultBackgroundTexture(Identifier texture);
+    
+    Runnable getSavingRunnable();
+    
+    ConfigBuilder setSavingRunnable(Runnable runnable);
+    
+    Consumer<Screen> getAfterInitConsumer();
+    
+    ConfigBuilder setAfterInitConsumer(Consumer<Screen> afterInitConsumer);
+    
+    Screen build();
+    
+}

+ 16 - 0
src/main/java/me/shedaniel/clothconfig2/api/ConfigCategory.java

@@ -0,0 +1,16 @@
+package me.shedaniel.clothconfig2.api;
+
+import net.minecraft.util.Identifier;
+
+import java.util.List;
+
+public interface ConfigCategory {
+    
+    @Deprecated
+    List<Object> getEntries();
+    
+    ConfigCategory addEntry(AbstractConfigListEntry entry);
+    
+    ConfigCategory setCategoryBackground(Identifier identifier);
+    
+}

+ 41 - 0
src/main/java/me/shedaniel/clothconfig2/api/ConfigEntryBuilder.java

@@ -0,0 +1,41 @@
+package me.shedaniel.clothconfig2.api;
+
+import me.shedaniel.clothconfig2.impl.ConfigEntryBuilderImpl;
+import me.shedaniel.clothconfig2.impl.builders.*;
+
+import java.util.List;
+
+public interface ConfigEntryBuilder {
+    
+    static ConfigEntryBuilder create() {
+        return ConfigEntryBuilderImpl.create();
+    }
+    
+    String getResetButtonKey();
+    
+    ConfigEntryBuilder setResetButtonKey(String resetButtonKey);
+    
+    SubCategoryBuilder startSubCategory(String fieldNameKey);
+    
+    SubCategoryBuilder startSubCategory(String fieldNameKey, List<AbstractConfigListEntry> entries);
+    
+    BooleanToggleBuilder startBooleanToggle(String fieldNameKey, boolean value);
+    
+    TextFieldBuilder startTextField(String fieldNameKey, String value);
+    
+    TextDescriptionBuilder startTextDescription(String value);
+    
+    <T extends Enum<?>> EnumSelectorBuilder<T> startEnumSelector(String fieldNameKey, Class<T> clazz, T value);
+    
+    IntFieldBuilder startIntField(String fieldNameKey, int value);
+    
+    LongFieldBuilder startLongField(String fieldNameKey, long value);
+    
+    FloatFieldBuilder startFloatField(String fieldNameKey, float value);
+    
+    DoubleFieldBuilder startDoubleField(String fieldNameKey, double value);
+    
+    IntSliderBuilder startIntSlider(String fieldNameKey, int value, int min, int max);
+    
+    LongSliderBuilder startLongSlider(String fieldNameKey, long value, long min, long max);
+}

+ 43 - 0
src/main/java/me/shedaniel/clothconfig2/api/QueuedTooltip.java

@@ -0,0 +1,43 @@
+package me.shedaniel.clothconfig2.api;
+
+import com.google.common.collect.Lists;
+
+import java.awt.*;
+import java.util.Collections;
+import java.util.List;
+
+public class QueuedTooltip {
+    
+    private Point location;
+    private List<String> text;
+    
+    private QueuedTooltip(Point location, List<String> text) {
+        this.location = location;
+        this.text = Collections.unmodifiableList(text);
+    }
+    
+    public static QueuedTooltip create(Point location, List<String> text) {
+        return new QueuedTooltip(location, text);
+    }
+    
+    public static QueuedTooltip create(Point location, String... text) {
+        return QueuedTooltip.create(location, Lists.newArrayList(text));
+    }
+    
+    public Point getLocation() {
+        return location;
+    }
+    
+    public int getX() {
+        return getLocation().x;
+    }
+    
+    public int getY() {
+        return getLocation().y;
+    }
+    
+    public List<String> getText() {
+        return text;
+    }
+    
+}

+ 454 - 0
src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java

@@ -0,0 +1,454 @@
+package me.shedaniel.clothconfig2.gui;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.AtomicDouble;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.platform.GlStateManager.DestFactor;
+import com.mojang.blaze3d.platform.GlStateManager.SourceFactor;
+import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
+import me.shedaniel.clothconfig2.api.AbstractConfigEntry;
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.api.QueuedTooltip;
+import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.ConfirmScreen;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.AbstractButtonWidget;
+import net.minecraft.client.gui.widget.AbstractPressableButtonWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.network.chat.TextComponent;
+import net.minecraft.network.chat.TranslatableComponent;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+import net.minecraft.util.Tickable;
+import net.minecraft.util.math.MathHelper;
+
+import java.awt.*;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public abstract class ClothConfigScreen extends Screen {
+    
+    private static final Identifier CONFIG_TEX = new Identifier("cloth-config2", "textures/gui/cloth_config.png");
+    private final List<QueuedTooltip> queuedTooltips = Lists.newArrayList();
+    public int nextTabIndex;
+    public int selectedTabIndex;
+    public double tabsScrollVelocity = 0d;
+    public double tabsScrollProgress = 0d;
+    public ListWidget listWidget;
+    private Screen parent;
+    private LinkedHashMap<String, List<AbstractConfigEntry>> tabbedEntries;
+    private List<Pair<String, Integer>> tabs;
+    private boolean edited;
+    private boolean confirmSave;
+    private AbstractButtonWidget buttonQuit;
+    private AbstractButtonWidget buttonSave;
+    private AbstractButtonWidget buttonLeftTab;
+    private AbstractButtonWidget buttonRightTab;
+    private Rectangle tabsBounds, tabsLeftBounds, tabsRightBounds;
+    private String title;
+    private double tabsMaximumScrolled = -1d;
+    private boolean displayErrors;
+    private List<ClothConfigTabButton> tabButtons;
+    private boolean smoothScrollingTabs = true;
+    private boolean smoothScrollingList = true;
+    private Identifier defaultBackgroundLocation;
+    private Map<String, Identifier> categoryBackgroundLocation;
+    
+    public ClothConfigScreen(Screen parent, String title, Map<String, List<Pair<String, Object>>> o) {
+        this(parent, title, o, true, true);
+    }
+    
+    public ClothConfigScreen(Screen parent, String title, Map<String, List<Pair<String, Object>>> o, boolean confirmSave, boolean displayErrors) {
+        this(parent, title, o, confirmSave, displayErrors, true, DrawableHelper.BACKGROUND_LOCATION);
+    }
+    
+    public ClothConfigScreen(Screen parent, String title, Map<String, List<Pair<String, Object>>> o, boolean confirmSave, boolean displayErrors, boolean smoothScrollingList, Identifier defaultBackgroundLocation) {
+        this(parent, title, o, confirmSave, displayErrors, smoothScrollingList, defaultBackgroundLocation, Maps.newHashMap());
+    }
+    
+    @SuppressWarnings("deprecation")
+    public ClothConfigScreen(Screen parent, String title, Map<String, List<Pair<String, Object>>> o, boolean confirmSave, boolean displayErrors, boolean smoothScrollingList, Identifier defaultBackgroundLocation, Map<String, Identifier> categoryBackgroundLocation) {
+        super(new TextComponent(""));
+        this.parent = parent;
+        this.title = title;
+        this.tabbedEntries = Maps.newLinkedHashMap();
+        this.smoothScrollingList = smoothScrollingList;
+        this.defaultBackgroundLocation = defaultBackgroundLocation;
+        o.forEach((tab, pairs) -> {
+            List<AbstractConfigEntry> list = Lists.newArrayList();
+            for(Pair<String, Object> pair : pairs) {
+                if (pair.getRight() instanceof AbstractConfigListEntry) {
+                    list.add((AbstractConfigListEntry) pair.getRight());
+                } else {
+                    throw new IllegalArgumentException("Unsupported Type (" + pair.getLeft() + "): " + pair.getRight().getClass().getSimpleName());
+                }
+            }
+            list.forEach(entry -> entry.setScreen(this));
+            tabbedEntries.put(tab, list);
+        });
+        this.nextTabIndex = 0;
+        this.selectedTabIndex = 0;
+        this.confirmSave = confirmSave;
+        this.edited = false;
+        TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+        this.tabs = tabbedEntries.keySet().stream().map(s -> new Pair<>(s, textRenderer.getStringWidth(I18n.translate(s)) + 8)).collect(Collectors.toList());
+        this.tabsScrollProgress = 0d;
+        this.tabButtons = Lists.newArrayList();
+        this.displayErrors = displayErrors;
+        this.categoryBackgroundLocation = categoryBackgroundLocation;
+    }
+    
+    @Override
+    public void tick() {
+        super.tick();
+        for(Element child : children())
+            if (child instanceof Tickable)
+                ((Tickable) child).tick();
+    }
+    
+    public Identifier getBackgroundLocation() {
+        if (categoryBackgroundLocation.containsKey(Lists.newArrayList(tabbedEntries.keySet()).get(selectedTabIndex)))
+            return categoryBackgroundLocation.get(Lists.newArrayList(tabbedEntries.keySet()).get(selectedTabIndex));
+        return defaultBackgroundLocation;
+    }
+    
+    public boolean isSmoothScrollingList() {
+        return smoothScrollingList;
+    }
+    
+    public void setSmoothScrollingList(boolean smoothScrollingList) {
+        this.smoothScrollingList = smoothScrollingList;
+    }
+    
+    public boolean isSmoothScrollingTabs() {
+        return smoothScrollingTabs;
+    }
+    
+    public void setSmoothScrollingTabs(boolean smoothScrolling) {
+        this.smoothScrollingTabs = smoothScrolling;
+    }
+    
+    public boolean isEdited() {
+        return edited;
+    }
+    
+    public void setEdited(boolean edited) {
+        this.edited = edited;
+        buttonQuit.setMessage(edited ? I18n.translate("text.cloth-config.cancel_discard") : I18n.translate("gui.cancel"));
+        buttonSave.active = edited;
+    }
+    
+    @Override
+    protected void init() {
+        super.init();
+        this.children.clear();
+        this.tabButtons.clear();
+        if (listWidget != null)
+            tabbedEntries.put(tabs.get(selectedTabIndex).getLeft(), listWidget.children());
+        selectedTabIndex = nextTabIndex;
+        children.add(listWidget = new ListWidget(minecraft, width, height, 70, height - 32, getBackgroundLocation()));
+        listWidget.setSmoothScrolling(this.smoothScrollingList);
+        if (tabbedEntries.size() > selectedTabIndex)
+            Lists.newArrayList(tabbedEntries.values()).get(selectedTabIndex).forEach(entry -> listWidget.children().add(entry));
+        addButton(buttonQuit = new ButtonWidget(width / 2 - 154, height - 26, 150, 20, edited ? I18n.translate("text.cloth-config.cancel_discard") : I18n.translate("gui.cancel"), widget -> {
+            if (confirmSave && edited)
+                minecraft.openScreen(new ConfirmScreen(new QuitSaveConsumer(), new TranslatableComponent("text.cloth-config.quit_config"), new TranslatableComponent("text.cloth-config.quit_config_sure"), I18n.translate("text.cloth-config.quit_discard"), I18n.translate("gui.cancel")));
+            else
+                minecraft.openScreen(parent);
+        }));
+        addButton(buttonSave = new AbstractPressableButtonWidget(width / 2 + 4, height - 26, 150, 20, "") {
+            @Override
+            public void onPress() {
+                Map<String, List<Pair<String, Object>>> map = Maps.newLinkedHashMap();
+                tabbedEntries.forEach((s, abstractListEntries) -> {
+                    List list = abstractListEntries.stream().map(entry -> new Pair(entry.getFieldName(), entry.getValue())).collect(Collectors.toList());
+                    map.put(s, list);
+                });
+                for(List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values()))
+                    for(AbstractConfigEntry entry : entries)
+                        entry.save();
+                onSave(map);
+                ClothConfigScreen.this.minecraft.openScreen(parent);
+            }
+            
+            @Override
+            public void render(int int_1, int int_2, float float_1) {
+                boolean hasErrors = false;
+                if (displayErrors)
+                    for(List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values())) {
+                        for(AbstractConfigEntry entry : entries)
+                            if (entry.getError().isPresent()) {
+                                hasErrors = true;
+                                break;
+                            }
+                        if (hasErrors)
+                            break;
+                    }
+                active = edited && !hasErrors;
+                setMessage(displayErrors && hasErrors ? I18n.translate("text.cloth-config.error_cannot_save") : I18n.translate("text.cloth-config.save_and_done"));
+                super.render(int_1, int_2, float_1);
+            }
+        });
+        buttonSave.active = edited;
+        tabsBounds = new Rectangle(0, 41, width, 24);
+        tabsLeftBounds = new Rectangle(0, 41, 18, 24);
+        tabsRightBounds = new Rectangle(width - 18, 41, 18, 24);
+        children.add(buttonLeftTab = new AbstractPressableButtonWidget(4, 44, 12, 18, "") {
+            @Override
+            public void onPress() {
+                tabsScrollProgress = Integer.MIN_VALUE;
+                tabsScrollVelocity = 0d;
+                clampTabsScrolled();
+            }
+            
+            @Override
+            public void renderButton(int int_1, int int_2, float float_1) {
+                minecraft.getTextureManager().bindTexture(CONFIG_TEX);
+                GlStateManager.color4f(1.0F, 1.0F, 1.0F, this.alpha);
+                int int_3 = this.getYImage(this.isHovered());
+                GlStateManager.enableBlend();
+                GlStateManager.blendFuncSeparate(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ONE, DestFactor.ZERO);
+                GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+                this.blit(x, y, 12, 18 * int_3, width, height);
+            }
+        });
+        int j = 0;
+        for(Pair<String, Integer> tab : tabs) {
+            tabButtons.add(new ClothConfigTabButton(this, j, -100, 43, tab.getRight(), 20, I18n.translate(tab.getLeft())));
+            j++;
+        }
+        tabButtons.forEach(children::add);
+        children.add(buttonRightTab = new AbstractPressableButtonWidget(width - 16, 44, 12, 18, "") {
+            @Override
+            public void onPress() {
+                tabsScrollProgress = Integer.MAX_VALUE;
+                tabsScrollVelocity = 0d;
+                clampTabsScrolled();
+            }
+            
+            @Override
+            public void renderButton(int int_1, int int_2, float float_1) {
+                minecraft.getTextureManager().bindTexture(CONFIG_TEX);
+                GlStateManager.color4f(1.0F, 1.0F, 1.0F, this.alpha);
+                int int_3 = this.getYImage(this.isHovered());
+                GlStateManager.enableBlend();
+                GlStateManager.blendFuncSeparate(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ONE, DestFactor.ZERO);
+                GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+                this.blit(x, y, 0, 18 * int_3, width, height);
+            }
+        });
+    }
+    
+    @Override
+    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        if (tabsBounds.contains(double_1, double_2) && !tabsLeftBounds.contains(double_1, double_2) && !tabsRightBounds.contains(double_1, double_2) && double_3 != 0d) {
+            if (double_3 < 0)
+                tabsScrollVelocity += 16;
+            if (double_3 > 0)
+                tabsScrollVelocity -= 16;
+            return true;
+        }
+        return super.mouseScrolled(double_1, double_2, double_3);
+    }
+    
+    public double getTabsMaximumScrolled() {
+        if (tabsMaximumScrolled == -1d) {
+            AtomicDouble d = new AtomicDouble();
+            tabs.forEach(pair -> d.addAndGet(pair.getRight() + 2));
+            tabsMaximumScrolled = d.get();
+        }
+        return tabsMaximumScrolled;
+    }
+    
+    public void resetTabsMaximumScrolled() {
+        tabsMaximumScrolled = -1d;
+        tabsScrollVelocity = 0f;
+    }
+    
+    public void clampTabsScrolled() {
+        int xx = 0;
+        for(ClothConfigTabButton tabButton : tabButtons)
+            xx += tabButton.getWidth() + 2;
+        if (xx > width - 40)
+            tabsScrollProgress = MathHelper.clamp(tabsScrollProgress, 0, getTabsMaximumScrolled() - width + 40);
+        else
+            tabsScrollProgress = 0d;
+    }
+    
+    @Override
+    public void render(int int_1, int int_2, float float_1) {
+        if (smoothScrollingTabs) {
+            double change = tabsScrollVelocity * 0.2f;
+            if (change != 0) {
+                if (change > 0 && change < .2)
+                    change = .2;
+                else if (change < 0 && change > -.2)
+                    change = -.2;
+                tabsScrollProgress += change;
+                tabsScrollVelocity -= change;
+                if (change > 0 == tabsScrollVelocity < 0)
+                    tabsScrollVelocity = 0f;
+                clampTabsScrolled();
+            }
+        } else {
+            tabsScrollProgress += tabsScrollVelocity;
+            tabsScrollVelocity = 0d;
+            clampTabsScrolled();
+        }
+        int xx = 20 - (int) tabsScrollProgress;
+        for(ClothConfigTabButton tabButton : tabButtons) {
+            tabButton.x = xx;
+            xx += tabButton.getWidth() + 2;
+        }
+        buttonLeftTab.active = tabsScrollProgress > 0d;
+        buttonRightTab.active = tabsScrollProgress < getTabsMaximumScrolled() - width + 40;
+        renderDirtBackground(0);
+        listWidget.render(int_1, int_2, float_1);
+        overlayBackground(tabsBounds, 32, 32, 32, 255, 255);
+        
+        drawCenteredString(minecraft.textRenderer, title, width / 2, 18, -1);
+        tabButtons.forEach(widget -> widget.render(int_1, int_2, float_1));
+        overlayBackground(tabsLeftBounds, 64, 64, 64, 255, 255);
+        overlayBackground(tabsRightBounds, 64, 64, 64, 255, 255);
+        drawShades();
+        buttonLeftTab.render(int_1, int_2, float_1);
+        buttonRightTab.render(int_1, int_2, float_1);
+        
+        if (displayErrors && isEditable()) {
+            List<String> errors = Lists.newArrayList();
+            for(List<AbstractConfigEntry> entries : Lists.newArrayList(tabbedEntries.values()))
+                for(AbstractConfigEntry entry : entries)
+                    if (entry.getError().isPresent())
+                        errors.add(((Optional<String>) entry.getError()).get());
+            if (errors.size() > 0) {
+                minecraft.getTextureManager().bindTexture(CONFIG_TEX);
+                GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+                blit(10, 10, 0, 54, 3, 11);
+                if (errors.size() == 1)
+                    drawString(minecraft.textRenderer, "§c" + errors.get(0), 18, 12, -1);
+                else
+                    drawString(minecraft.textRenderer, "§c" + I18n.translate("text.cloth-config.multi_error"), 18, 12, -1);
+            }
+        } else if (!isEditable()) {
+            minecraft.getTextureManager().bindTexture(CONFIG_TEX);
+            GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+            blit(10, 10, 0, 54, 3, 11);
+            drawString(minecraft.textRenderer, "§c" + I18n.translate("text.cloth-config.not_editable"), 18, 12, -1);
+        }
+        super.render(int_1, int_2, float_1);
+        queuedTooltips.forEach(queuedTooltip -> renderTooltip(queuedTooltip.getText(), queuedTooltip.getX(), queuedTooltip.getY()));
+        queuedTooltips.clear();
+    }
+    
+    public void queueTooltip(QueuedTooltip queuedTooltip) {
+        queuedTooltips.add(queuedTooltip);
+    }
+    
+    private void drawShades() {
+        GlStateManager.enableBlend();
+        GlStateManager.blendFuncSeparate(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ZERO, DestFactor.ONE);
+        GlStateManager.disableAlphaTest();
+        GlStateManager.shadeModel(7425);
+        GlStateManager.disableTexture();
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder buffer = tessellator.getBufferBuilder();
+        buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+        buffer.vertex(tabsBounds.getMinX() + 20, tabsBounds.getMinY() + 4, 0.0D).texture(0.0D, 1.0D).color(0, 0, 0, 0).next();
+        buffer.vertex(tabsBounds.getMaxX() - 20, tabsBounds.getMinY() + 4, 0.0D).texture(1.0D, 1.0D).color(0, 0, 0, 0).next();
+        buffer.vertex(tabsBounds.getMaxX() - 20, tabsBounds.getMinY(), 0.0D).texture(1.0D, 0.0D).color(0, 0, 0, 255).next();
+        buffer.vertex(tabsBounds.getMinX() + 20, tabsBounds.getMinY(), 0.0D).texture(0.0D, 0.0D).color(0, 0, 0, 255).next();
+        tessellator.draw();
+        buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+        buffer.vertex(tabsBounds.getMinX() + 20, tabsBounds.getMaxY(), 0.0D).texture(0.0D, 1.0D).color(0, 0, 0, 255).next();
+        buffer.vertex(tabsBounds.getMaxX() - 20, tabsBounds.getMaxY(), 0.0D).texture(1.0D, 1.0D).color(0, 0, 0, 255).next();
+        buffer.vertex(tabsBounds.getMaxX() - 20, tabsBounds.getMaxY() - 4, 0.0D).texture(1.0D, 0.0D).color(0, 0, 0, 0).next();
+        buffer.vertex(tabsBounds.getMinX() + 20, tabsBounds.getMaxY() - 4, 0.0D).texture(0.0D, 0.0D).color(0, 0, 0, 0).next();
+        tessellator.draw();
+        GlStateManager.enableTexture();
+        GlStateManager.shadeModel(7424);
+        GlStateManager.enableAlphaTest();
+        GlStateManager.disableBlend();
+    }
+    
+    protected void overlayBackground(Rectangle rect, int red, int green, int blue, int startAlpha, int endAlpha) {
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder buffer = tessellator.getBufferBuilder();
+        minecraft.getTextureManager().bindTexture(getBackgroundLocation());
+        GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        float f = 32.0F;
+        buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+        buffer.vertex(rect.getMinX(), rect.getMaxY(), 0.0D).texture(rect.getMinX() / 32.0D, rect.getMaxY() / 32.0D).color(red, green, blue, endAlpha).next();
+        buffer.vertex(rect.getMaxX(), rect.getMaxY(), 0.0D).texture(rect.getMaxX() / 32.0D, rect.getMaxY() / 32.0D).color(red, green, blue, endAlpha).next();
+        buffer.vertex(rect.getMaxX(), rect.getMinY(), 0.0D).texture(rect.getMaxX() / 32.0D, rect.getMinY() / 32.0D).color(red, green, blue, startAlpha).next();
+        buffer.vertex(rect.getMinX(), rect.getMinY(), 0.0D).texture(rect.getMinX() / 32.0D, rect.getMinY() / 32.0D).color(red, green, blue, startAlpha).next();
+        tessellator.draw();
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if (int_1 == 256 && this.shouldCloseOnEsc()) {
+            if (confirmSave && edited)
+                minecraft.openScreen(new ConfirmScreen(new QuitSaveConsumer(), new TranslatableComponent("text.cloth-config.quit_config"), new TranslatableComponent("text.cloth-config.quit_config_sure"), I18n.translate("text.cloth-config.quit_discard"), I18n.translate("gui.cancel")));
+            else
+                minecraft.openScreen(parent);
+            return true;
+        }
+        return super.keyPressed(int_1, int_2, int_3);
+    }
+    
+    public abstract void onSave(Map<String, List<Pair<String, Object>>> o);
+    
+    public boolean isEditable() {
+        return true;
+    }
+    
+    private class QuitSaveConsumer implements BooleanConsumer {
+        @Override
+        public void accept(boolean t) {
+            if (!t)
+                minecraft.openScreen(ClothConfigScreen.this);
+            else
+                minecraft.openScreen(parent);
+            return;
+        }
+    }
+    
+    public class ListWidget extends DynamicElementListWidget {
+        
+        public ListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+            super(client, width, height, top, bottom, backgroundLocation);
+            visible = false;
+        }
+        
+        @Override
+        public int getItemWidth() {
+            return width - 80;
+        }
+        
+        public ClothConfigScreen getScreen() {
+            return ClothConfigScreen.this;
+        }
+        
+        @Override
+        protected int getScrollbarPosition() {
+            return width - 36;
+        }
+        
+        protected final void clearStuff() {
+            this.clearItems();
+        }
+    }
+    
+}

+ 39 - 0
src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigTabButton.java

@@ -0,0 +1,39 @@
+package me.shedaniel.clothconfig2.gui;
+
+import net.minecraft.client.gui.widget.AbstractPressableButtonWidget;
+
+public class ClothConfigTabButton extends AbstractPressableButtonWidget {
+    
+    private int index = -1;
+    private ClothConfigScreen screen;
+    
+    public ClothConfigTabButton(ClothConfigScreen screen, int index, int int_1, int int_2, int int_3, int int_4, String string_1) {
+        super(int_1, int_2, int_3, int_4, string_1);
+        this.index = index;
+        this.screen = screen;
+    }
+    
+    @Override
+    public void onPress() {
+        if (index != -1)
+            screen.nextTabIndex = index;
+        screen.tabsScrollVelocity = 0d;
+        screen.init();
+    }
+    
+    @Override
+    public void render(int int_1, int int_2, float float_1) {
+        active = index != screen.selectedTabIndex;
+        super.render(int_1, int_2, float_1);
+    }
+    
+    @Override
+    protected boolean clicked(double double_1, double double_2) {
+        return visible && active && isMouseOver(double_1, double_2);
+    }
+    
+    @Override
+    public boolean isMouseOver(double double_1, double double_2) {
+        return this.active && this.visible && double_1 >= this.x && double_2 >= this.y && double_1 < this.x + this.width && double_2 < this.y + this.height && double_1 >= 20 && double_1 < screen.width - 20;
+    }
+}

+ 97 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/BooleanListEntry.java

@@ -0,0 +1,97 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import com.google.common.collect.Lists;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.Window;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class BooleanListEntry extends TooltipListEntry<Boolean> {
+    
+    private AtomicBoolean bool;
+    private ButtonWidget buttonWidget, resetButton;
+    private Consumer<Boolean> saveConsumer;
+    private Supplier<Boolean> defaultValue;
+    private List<Element> widgets;
+    
+    public BooleanListEntry(String fieldName, boolean bool, Consumer<Boolean> saveConsumer) {
+        this(fieldName, bool, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public BooleanListEntry(String fieldName, boolean bool, String resetButtonKey, Supplier<Boolean> defaultValue, Consumer<Boolean> saveConsumer) {
+        this(fieldName, bool, resetButtonKey, defaultValue, saveConsumer, null);
+    }
+    
+    public BooleanListEntry(String fieldName, boolean bool, String resetButtonKey, Supplier<Boolean> defaultValue, Consumer<Boolean> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, tooltipSupplier);
+        this.defaultValue = defaultValue;
+        this.bool = new AtomicBoolean(bool);
+        this.buttonWidget = new ButtonWidget(0, 0, 150, 20, "", widget -> {
+            BooleanListEntry.this.bool.set(!BooleanListEntry.this.bool.get());
+            getScreen().setEdited(true);
+        });
+        this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(resetButtonKey)) + 6, 20, I18n.translate(resetButtonKey), widget -> {
+            BooleanListEntry.this.bool.set(defaultValue.get());
+            getScreen().setEdited(true);
+        });
+        this.saveConsumer = saveConsumer;
+        this.widgets = Lists.newArrayList(buttonWidget, resetButton);
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    @Override
+    public Boolean getValue() {
+        return bool.get();
+    }
+    
+    @Override
+    public Optional<Boolean> getDefaultValue() {
+        return defaultValue == null ? Optional.empty() : Optional.ofNullable(defaultValue.get());
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        Window window = MinecraftClient.getInstance().window;
+        this.resetButton.active = isEditable() && getDefaultValue().isPresent() && defaultValue.get().booleanValue() != bool.get();
+        this.resetButton.y = y;
+        this.buttonWidget.active = isEditable();
+        this.buttonWidget.y = y;
+        this.buttonWidget.setMessage(getYesNoText(bool.get()));
+        if (MinecraftClient.getInstance().textRenderer.isRightToLeft()) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), window.getScaledWidth() - x - MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(getFieldName())), y + 5, 16777215);
+            this.resetButton.x = x;
+            this.buttonWidget.x = x + resetButton.getWidth() + 2;
+            this.buttonWidget.setWidth(150 - resetButton.getWidth() - 2);
+        } else {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), x, y + 5, 16777215);
+            this.resetButton.x = x + entryWidth - resetButton.getWidth();
+            this.buttonWidget.x = x + entryWidth - 150;
+            this.buttonWidget.setWidth(150 - resetButton.getWidth() - 2);
+        }
+        resetButton.render(mouseX, mouseY, delta);
+        buttonWidget.render(mouseX, mouseY, delta);
+    }
+    
+    public String getYesNoText(boolean bool) {
+        return bool ? "§aYes" : "§cNo";
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return widgets;
+    }
+    
+}

+ 106 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/DoubleListEntry.java

@@ -0,0 +1,106 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.resource.language.I18n;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class DoubleListEntry extends TextFieldListEntry<Double> {
+    
+    private static Function<String, String> stripCharacters = s -> {
+        StringBuilder stringBuilder_1 = new StringBuilder();
+        char[] var2 = s.toCharArray();
+        int var3 = var2.length;
+        
+        for(int var4 = 0; var4 < var3; ++var4)
+            if (Character.isDigit(var2[var4]) || var2[var4] == '-' || var2[var4] == '.')
+                stringBuilder_1.append(var2[var4]);
+        
+        return stringBuilder_1.toString();
+    };
+    private double minimum, maximum;
+    private Consumer<Double> saveConsumer;
+    
+    public DoubleListEntry(String fieldName, Double value, Consumer<Double> saveConsumer) {
+        this(fieldName, value, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public DoubleListEntry(String fieldName, Double value, String resetButtonKey, Supplier<Double> defaultValue, Consumer<Double> saveConsumer) {
+        super(fieldName, value, resetButtonKey, defaultValue);
+        this.minimum = -Double.MAX_VALUE;
+        this.maximum = Double.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    public DoubleListEntry(String fieldName, Double value, String resetButtonKey, Supplier<Double> defaultValue, Consumer<Double> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, value, resetButtonKey, defaultValue, tooltipSupplier);
+        this.minimum = -Double.MAX_VALUE;
+        this.maximum = Double.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    @Override
+    protected String stripAddText(String s) {
+        return stripCharacters.apply(s);
+    }
+    
+    @Override
+    protected void textFieldPreRender(TextFieldWidget widget) {
+        try {
+            double i = Double.valueOf(textFieldWidget.getText());
+            if (i < minimum || i > maximum)
+                widget.setEditableColor(16733525);
+            else
+                widget.setEditableColor(14737632);
+        } catch (NumberFormatException ex) {
+            widget.setEditableColor(16733525);
+        }
+    }
+    
+    @Override
+    protected boolean isMatchDefault(String text) {
+        return getDefaultValue().isPresent() ? text.equals(defaultValue.get().toString()) : false;
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    public DoubleListEntry setMinimum(double minimum) {
+        this.minimum = minimum;
+        return this;
+    }
+    
+    public DoubleListEntry setMaximum(double maximum) {
+        this.maximum = maximum;
+        return this;
+    }
+    
+    @Override
+    public Double getValue() {
+        try {
+            return Double.valueOf(textFieldWidget.getText());
+        } catch (Exception e) {
+            return 0d;
+        }
+    }
+    
+    @Override
+    public Optional<String> getError() {
+        try {
+            double i = Double.valueOf(textFieldWidget.getText());
+            if (i > maximum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_large", maximum));
+            else if (i < minimum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_small", minimum));
+        } catch (NumberFormatException ex) {
+            return Optional.of(I18n.translate("text.cloth-config.error.not_valid_number_double"));
+        }
+        return super.getError();
+    }
+}

+ 118 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/EnumListEntry.java

@@ -0,0 +1,118 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.Window;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class EnumListEntry<T extends Enum<?>> extends TooltipListEntry<T> {
+    
+    public static final Function<Enum, String> DEFAULT_NAME_PROVIDER = t -> I18n.translate(t instanceof Translatable ? ((Translatable) t).getKey() : t.toString());
+    private ImmutableList<T> values;
+    private AtomicInteger index;
+    private ButtonWidget buttonWidget, resetButton;
+    private Consumer<T> saveConsumer;
+    private Supplier<T> defaultValue;
+    private List<Element> widgets;
+    private Function<Enum, String> enumNameProvider;
+    
+    public EnumListEntry(String fieldName, Class<T> clazz, T value, Consumer<T> saveConsumer) {
+        this(fieldName, clazz, value, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public EnumListEntry(String fieldName, Class<T> clazz, T value, String resetButtonKey, Supplier<T> defaultValue, Consumer<T> saveConsumer) {
+        this(fieldName, clazz, value, resetButtonKey, defaultValue, saveConsumer, DEFAULT_NAME_PROVIDER);
+    }
+    
+    public EnumListEntry(String fieldName, Class<T> clazz, T value, String resetButtonKey, Supplier<T> defaultValue, Consumer<T> saveConsumer, Function<Enum, String> enumNameProvider) {
+        this(fieldName, clazz, value, resetButtonKey, defaultValue, saveConsumer, enumNameProvider, null);
+    }
+    
+    public EnumListEntry(String fieldName, Class<T> clazz, T value, String resetButtonKey, Supplier<T> defaultValue, Consumer<T> saveConsumer, Function<Enum, String> enumNameProvider, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, tooltipSupplier);
+        T[] valuesArray = clazz.getEnumConstants();
+        if (valuesArray != null)
+            this.values = ImmutableList.copyOf(valuesArray);
+        else
+            this.values = ImmutableList.of(value);
+        this.defaultValue = defaultValue;
+        this.index = new AtomicInteger(this.values.indexOf(value));
+        this.index.compareAndSet(-1, 0);
+        this.buttonWidget = new ButtonWidget(0, 0, 150, 20, "", widget -> {
+            EnumListEntry.this.index.incrementAndGet();
+            EnumListEntry.this.index.compareAndSet(EnumListEntry.this.values.size(), 0);
+            getScreen().setEdited(true);
+        });
+        this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(resetButtonKey)) + 6, 20, I18n.translate(resetButtonKey), widget -> {
+            EnumListEntry.this.index.set(getDefaultIndex());
+            getScreen().setEdited(true);
+        });
+        this.saveConsumer = saveConsumer;
+        this.widgets = Lists.newArrayList(buttonWidget, resetButton);
+        this.enumNameProvider = enumNameProvider;
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    @Override
+    public T getValue() {
+        return this.values.get(this.index.get());
+    }
+    
+    @Override
+    public Optional<T> getDefaultValue() {
+        return defaultValue == null ? Optional.empty() : Optional.ofNullable(defaultValue.get());
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        Window window = MinecraftClient.getInstance().window;
+        this.resetButton.active = isEditable() && getDefaultValue().isPresent() && getDefaultIndex() != this.index.get();
+        this.resetButton.y = y;
+        this.buttonWidget.active = isEditable();
+        this.buttonWidget.y = y;
+        this.buttonWidget.setMessage(enumNameProvider.apply(getValue()));
+        if (MinecraftClient.getInstance().textRenderer.isRightToLeft()) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), window.getScaledWidth() - x - MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(getFieldName())), y + 5, 16777215);
+            this.resetButton.x = x;
+            this.buttonWidget.x = x + resetButton.getWidth() + 2;
+            this.buttonWidget.setWidth(150 - resetButton.getWidth() - 2);
+        } else {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), x, y + 5, 16777215);
+            this.resetButton.x = x + entryWidth - resetButton.getWidth();
+            this.buttonWidget.x = x + entryWidth - 150;
+            this.buttonWidget.setWidth(150 - resetButton.getWidth() - 2);
+        }
+        resetButton.render(mouseX, mouseY, delta);
+        buttonWidget.render(mouseX, mouseY, delta);
+    }
+    
+    private int getDefaultIndex() {
+        return Math.max(0, this.values.indexOf(this.defaultValue.get()));
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return widgets;
+    }
+    
+    public static interface Translatable {
+        String getKey();
+    }
+    
+}

+ 106 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/FloatListEntry.java

@@ -0,0 +1,106 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.resource.language.I18n;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class FloatListEntry extends TextFieldListEntry<Float> {
+    
+    private static Function<String, String> stripCharacters = s -> {
+        StringBuilder stringBuilder_1 = new StringBuilder();
+        char[] var2 = s.toCharArray();
+        int var3 = var2.length;
+        
+        for(int var4 = 0; var4 < var3; ++var4)
+            if (Character.isDigit(var2[var4]) || var2[var4] == '-' || var2[var4] == '.')
+                stringBuilder_1.append(var2[var4]);
+        
+        return stringBuilder_1.toString();
+    };
+    private float minimum, maximum;
+    private Consumer<Float> saveConsumer;
+    
+    public FloatListEntry(String fieldName, Float value, Consumer<Float> saveConsumer) {
+        this(fieldName, value, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public FloatListEntry(String fieldName, Float value, String resetButtonKey, Supplier<Float> defaultValue, Consumer<Float> saveConsumer) {
+        super(fieldName, value, resetButtonKey, defaultValue);
+        this.minimum = -Float.MAX_VALUE;
+        this.maximum = Float.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    public FloatListEntry(String fieldName, Float value, String resetButtonKey, Supplier<Float> defaultValue, Consumer<Float> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, value, resetButtonKey, defaultValue, tooltipSupplier);
+        this.minimum = -Float.MAX_VALUE;
+        this.maximum = Float.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    @Override
+    protected String stripAddText(String s) {
+        return stripCharacters.apply(s);
+    }
+    
+    @Override
+    protected void textFieldPreRender(TextFieldWidget widget) {
+        try {
+            double i = Float.valueOf(textFieldWidget.getText());
+            if (i < minimum || i > maximum)
+                widget.setEditableColor(16733525);
+            else
+                widget.setEditableColor(14737632);
+        } catch (NumberFormatException ex) {
+            widget.setEditableColor(16733525);
+        }
+    }
+    
+    @Override
+    protected boolean isMatchDefault(String text) {
+        return getDefaultValue().isPresent() ? text.equals(defaultValue.get().toString()) : false;
+    }
+    
+    public FloatListEntry setMinimum(float minimum) {
+        this.minimum = minimum;
+        return this;
+    }
+    
+    public FloatListEntry setMaximum(float maximum) {
+        this.maximum = maximum;
+        return this;
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    @Override
+    public Float getValue() {
+        try {
+            return Float.valueOf(textFieldWidget.getText());
+        } catch (Exception e) {
+            return 0f;
+        }
+    }
+    
+    @Override
+    public Optional<String> getError() {
+        try {
+            float i = Float.valueOf(textFieldWidget.getText());
+            if (i > maximum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_large", maximum));
+            else if (i < minimum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_small", minimum));
+        } catch (NumberFormatException ex) {
+            return Optional.of(I18n.translate("text.cloth-config.error.not_valid_number_float"));
+        }
+        return super.getError();
+    }
+}

+ 106 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/IntegerListEntry.java

@@ -0,0 +1,106 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.resource.language.I18n;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class IntegerListEntry extends TextFieldListEntry<Integer> {
+    
+    private static Function<String, String> stripCharacters = s -> {
+        StringBuilder stringBuilder_1 = new StringBuilder();
+        char[] var2 = s.toCharArray();
+        int var3 = var2.length;
+        
+        for(int var4 = 0; var4 < var3; ++var4)
+            if (Character.isDigit(var2[var4]) || var2[var4] == '-')
+                stringBuilder_1.append(var2[var4]);
+        
+        return stringBuilder_1.toString();
+    };
+    private int minimum, maximum;
+    private Consumer<Integer> saveConsumer;
+    
+    public IntegerListEntry(String fieldName, Integer value, Consumer<Integer> saveConsumer) {
+        this(fieldName, value, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public IntegerListEntry(String fieldName, Integer value, String resetButtonKey, Supplier<Integer> defaultValue, Consumer<Integer> saveConsumer) {
+        super(fieldName, value, resetButtonKey, defaultValue);
+        this.minimum = -Integer.MAX_VALUE;
+        this.maximum = Integer.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    public IntegerListEntry(String fieldName, Integer value, String resetButtonKey, Supplier<Integer> defaultValue, Consumer<Integer> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, value, resetButtonKey, defaultValue, tooltipSupplier);
+        this.minimum = -Integer.MAX_VALUE;
+        this.maximum = Integer.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    @Override
+    protected String stripAddText(String s) {
+        return stripCharacters.apply(s);
+    }
+    
+    @Override
+    protected void textFieldPreRender(TextFieldWidget widget) {
+        try {
+            double i = Integer.valueOf(textFieldWidget.getText());
+            if (i < minimum || i > maximum)
+                widget.setEditableColor(16733525);
+            else
+                widget.setEditableColor(14737632);
+        } catch (NumberFormatException ex) {
+            widget.setEditableColor(16733525);
+        }
+    }
+    
+    @Override
+    protected boolean isMatchDefault(String text) {
+        return getDefaultValue().isPresent() ? text.equals(defaultValue.get().toString()) : false;
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    public IntegerListEntry setMaximum(int maximum) {
+        this.maximum = maximum;
+        return this;
+    }
+    
+    public IntegerListEntry setMinimum(int minimum) {
+        this.minimum = minimum;
+        return this;
+    }
+    
+    @Override
+    public Integer getValue() {
+        try {
+            return Integer.valueOf(textFieldWidget.getText());
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+    
+    @Override
+    public Optional<String> getError() {
+        try {
+            int i = Integer.valueOf(textFieldWidget.getText());
+            if (i > maximum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_large", maximum));
+            else if (i < minimum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_small", minimum));
+        } catch (NumberFormatException ex) {
+            return Optional.of(I18n.translate("text.cloth-config.error.not_valid_number_int"));
+        }
+        return super.getError();
+    }
+}

+ 159 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/IntegerSliderEntry.java

@@ -0,0 +1,159 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import com.google.common.collect.Lists;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.SliderWidget;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.Window;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class IntegerSliderEntry extends TooltipListEntry<Integer> {
+    
+    protected Slider sliderWidget;
+    protected ButtonWidget resetButton;
+    protected AtomicInteger value;
+    private int minimum, maximum;
+    private Consumer<Integer> saveConsumer;
+    private Supplier<Integer> defaultValue;
+    private Function<Integer, String> textGetter = integer -> String.format("Value: %d", integer);
+    private List<Element> widgets;
+    
+    public IntegerSliderEntry(String fieldName, int minimum, int maximum, int value, Consumer<Integer> saveConsumer) {
+        this(fieldName, minimum, maximum, value, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public IntegerSliderEntry(String fieldName, int minimum, int maximum, int value, String resetButtonKey, Supplier<Integer> defaultValue, Consumer<Integer> saveConsumer) {
+        this(fieldName, minimum, maximum, value, resetButtonKey, defaultValue, saveConsumer, null);
+    }
+    
+    public IntegerSliderEntry(String fieldName, int minimum, int maximum, int value, String resetButtonKey, Supplier<Integer> defaultValue, Consumer<Integer> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, tooltipSupplier);
+        this.defaultValue = defaultValue;
+        this.value = new AtomicInteger(value);
+        this.saveConsumer = saveConsumer;
+        this.maximum = maximum;
+        this.minimum = minimum;
+        this.sliderWidget = new Slider(0, 0, 152, 20, ((double) this.value.get() - minimum) / Math.abs(maximum - minimum));
+        this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(resetButtonKey)) + 6, 20, I18n.translate(resetButtonKey), widget -> {
+            sliderWidget.setProgress((MathHelper.clamp(this.defaultValue.get(), minimum, maximum) - minimum) / (double) Math.abs(maximum - minimum));
+            this.value.set(MathHelper.clamp(this.defaultValue.get(), minimum, maximum));
+            sliderWidget.updateMessage();
+            getScreen().setEdited(true);
+        });
+        this.sliderWidget.setMessage(textGetter.apply(IntegerSliderEntry.this.value.get()));
+        this.widgets = Lists.newArrayList(sliderWidget, resetButton);
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    public Function<Integer, String> getTextGetter() {
+        return textGetter;
+    }
+    
+    public IntegerSliderEntry setTextGetter(Function<Integer, String> textGetter) {
+        this.textGetter = textGetter;
+        this.sliderWidget.setMessage(textGetter.apply(IntegerSliderEntry.this.value.get()));
+        return this;
+    }
+    
+    @Override
+    public Integer getValue() {
+        return value.get();
+    }
+    
+    @Override
+    public Optional<Integer> getDefaultValue() {
+        return defaultValue == null ? Optional.empty() : Optional.ofNullable(defaultValue.get());
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return widgets;
+    }
+    
+    public IntegerSliderEntry setMaximum(int maximum) {
+        this.maximum = maximum;
+        return this;
+    }
+    
+    public IntegerSliderEntry setMinimum(int minimum) {
+        this.minimum = minimum;
+        return this;
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        Window window = MinecraftClient.getInstance().window;
+        this.resetButton.active = isEditable() && getDefaultValue().isPresent() && defaultValue.get().intValue() != value.get();
+        this.resetButton.y = y;
+        this.sliderWidget.active = isEditable();
+        this.sliderWidget.y = y;
+        if (MinecraftClient.getInstance().textRenderer.isRightToLeft()) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), window.getScaledWidth() - x - MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(getFieldName())), y + 5, 16777215);
+            this.resetButton.x = x;
+            this.sliderWidget.x = x + resetButton.getWidth() + 1;
+            this.sliderWidget.setWidth(150 - resetButton.getWidth() - 2);
+        } else {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), x, y + 5, 16777215);
+            this.resetButton.x = x + entryWidth - resetButton.getWidth();
+            this.sliderWidget.x = x + entryWidth - 150;
+            this.sliderWidget.setWidth(150 - resetButton.getWidth() - 2);
+        }
+        resetButton.render(mouseX, mouseY, delta);
+        sliderWidget.render(mouseX, mouseY, delta);
+    }
+    
+    private class Slider extends SliderWidget {
+        protected Slider(int int_1, int int_2, int int_3, int int_4, double double_1) {
+            super(int_1, int_2, int_3, int_4, double_1);
+        }
+        
+        @Override
+        public void updateMessage() {
+            setMessage(textGetter.apply(IntegerSliderEntry.this.value.get()));
+        }
+        
+        @Override
+        protected void applyValue() {
+            IntegerSliderEntry.this.value.set((int) (minimum + Math.abs(maximum - minimum) * value));
+            getScreen().setEdited(true);
+        }
+        
+        @Override
+        public boolean keyPressed(int int_1, int int_2, int int_3) {
+            if (!isEditable())
+                return false;
+            return super.keyPressed(int_1, int_2, int_3);
+        }
+        
+        @Override
+        public boolean mouseDragged(double double_1, double double_2, int int_1, double double_3, double double_4) {
+            if (!isEditable())
+                return false;
+            return super.mouseDragged(double_1, double_2, int_1, double_3, double_4);
+        }
+        
+        public double getProgress() {
+            return value;
+        }
+        
+        public void setProgress(double integer) {
+            this.value = integer;
+        }
+    }
+    
+}

+ 106 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/LongListEntry.java

@@ -0,0 +1,106 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.resource.language.I18n;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class LongListEntry extends TextFieldListEntry<Long> {
+    
+    private static Function<String, String> stripCharacters = s -> {
+        StringBuilder stringBuilder_1 = new StringBuilder();
+        char[] var2 = s.toCharArray();
+        int var3 = var2.length;
+        
+        for(int var4 = 0; var4 < var3; ++var4)
+            if (Character.isDigit(var2[var4]) || var2[var4] == '-')
+                stringBuilder_1.append(var2[var4]);
+        
+        return stringBuilder_1.toString();
+    };
+    private long minimum, maximum;
+    private Consumer<Long> saveConsumer;
+    
+    public LongListEntry(String fieldName, Long value, Consumer<Long> saveConsumer) {
+        this(fieldName, value, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public LongListEntry(String fieldName, Long value, String resetButtonKey, Supplier<Long> defaultValue, Consumer<Long> saveConsumer) {
+        super(fieldName, value, resetButtonKey, defaultValue);
+        this.minimum = -Long.MAX_VALUE;
+        this.maximum = Long.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    public LongListEntry(String fieldName, Long value, String resetButtonKey, Supplier<Long> defaultValue, Consumer<Long> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, value, resetButtonKey, defaultValue, tooltipSupplier);
+        this.minimum = -Long.MAX_VALUE;
+        this.maximum = Long.MAX_VALUE;
+        this.saveConsumer = saveConsumer;
+    }
+    
+    @Override
+    protected String stripAddText(String s) {
+        return stripCharacters.apply(s);
+    }
+    
+    @Override
+    protected void textFieldPreRender(TextFieldWidget widget) {
+        try {
+            double i = Long.valueOf(textFieldWidget.getText());
+            if (i < minimum || i > maximum)
+                widget.setEditableColor(16733525);
+            else
+                widget.setEditableColor(14737632);
+        } catch (NumberFormatException ex) {
+            widget.setEditableColor(16733525);
+        }
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    @Override
+    protected boolean isMatchDefault(String text) {
+        return getDefaultValue().isPresent() ? text.equals(defaultValue.get().toString()) : false;
+    }
+    
+    public LongListEntry setMinimum(long minimum) {
+        this.minimum = minimum;
+        return this;
+    }
+    
+    public LongListEntry setMaximum(long maximum) {
+        this.maximum = maximum;
+        return this;
+    }
+    
+    @Override
+    public Long getValue() {
+        try {
+            return Long.valueOf(textFieldWidget.getText());
+        } catch (Exception e) {
+            return 0l;
+        }
+    }
+    
+    @Override
+    public Optional<String> getError() {
+        try {
+            long i = Long.valueOf(textFieldWidget.getText());
+            if (i > maximum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_large", maximum));
+            else if (i < minimum)
+                return Optional.of(I18n.translate("text.cloth-config.error.too_small", minimum));
+        } catch (NumberFormatException ex) {
+            return Optional.of(I18n.translate("text.cloth-config.error.not_valid_number_long"));
+        }
+        return super.getError();
+    }
+}

+ 160 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/LongSliderEntry.java

@@ -0,0 +1,160 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import com.google.common.collect.Lists;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.SliderWidget;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.Window;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class LongSliderEntry extends TooltipListEntry<Long> {
+    
+    protected Slider sliderWidget;
+    protected ButtonWidget resetButton;
+    protected AtomicLong value;
+    private long minimum, maximum;
+    private Consumer<Long> saveConsumer;
+    private Supplier<Long> defaultValue;
+    private Function<Long, String> textGetter = value -> String.format("Value: %d", value);
+    private List<Element> widgets;
+    
+    public LongSliderEntry(String fieldName, long minimum, long maximum, long value, Consumer<Long> saveConsumer) {
+        this(fieldName, minimum, maximum, value, saveConsumer, "text.cloth-config.reset_value", null);
+    }
+    
+    public LongSliderEntry(String fieldName, long minimum, long maximum, long value, Consumer<Long> saveConsumer, String resetButtonKey, Supplier<Long> defaultValue) {
+        this(fieldName, minimum, maximum, value, saveConsumer, resetButtonKey, defaultValue, null);
+    }
+    
+    public LongSliderEntry(String fieldName, long minimum, long maximum, long value, Consumer<Long> saveConsumer, String resetButtonKey, Supplier<Long> defaultValue, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, tooltipSupplier);
+        this.defaultValue = defaultValue;
+        this.value = new AtomicLong(value);
+        this.saveConsumer = saveConsumer;
+        this.maximum = maximum;
+        this.minimum = minimum;
+        this.sliderWidget = new Slider(0, 0, 152, 20, ((double) LongSliderEntry.this.value.get() - minimum) / Math.abs(maximum - minimum));
+        this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(resetButtonKey)) + 6, 20, I18n.translate(resetButtonKey), widget -> {
+            sliderWidget.setValue((MathHelper.clamp(this.defaultValue.get(), minimum, maximum) - minimum) / (double) Math.abs(maximum - minimum));
+            this.value.set(Math.min(Math.max(this.defaultValue.get(), minimum), maximum));
+            sliderWidget.updateMessage();
+            getScreen().setEdited(true);
+        });
+        this.sliderWidget.setMessage(textGetter.apply(LongSliderEntry.this.value.get()));
+        this.widgets = Lists.newArrayList(sliderWidget, resetButton);
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    public Function<Long, String> getTextGetter() {
+        return textGetter;
+    }
+    
+    public LongSliderEntry setTextGetter(Function<Long, String> textGetter) {
+        this.textGetter = textGetter;
+        this.sliderWidget.setMessage(textGetter.apply(LongSliderEntry.this.value.get()));
+        return this;
+    }
+    
+    @Override
+    public Long getValue() {
+        return value.get();
+    }
+    
+    @Override
+    public Optional<Long> getDefaultValue() {
+        return defaultValue == null ? Optional.empty() : Optional.ofNullable(defaultValue.get());
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return widgets;
+    }
+    
+    public LongSliderEntry setMaximum(long maximum) {
+        this.maximum = maximum;
+        return this;
+    }
+    
+    public LongSliderEntry setMinimum(long minimum) {
+        this.minimum = minimum;
+        return this;
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        Window window = MinecraftClient.getInstance().window;
+        this.resetButton.active = isEditable() && getDefaultValue().isPresent() && defaultValue.get().longValue() != value.get();
+        this.resetButton.y = y;
+        this.sliderWidget.active = isEditable();
+        this.sliderWidget.y = y;
+        if (MinecraftClient.getInstance().textRenderer.isRightToLeft()) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), window.getScaledWidth() - x - MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(getFieldName())), y + 5, 16777215);
+            this.resetButton.x = x;
+            this.sliderWidget.x = x + resetButton.getWidth() + 1;
+            this.sliderWidget.setWidth(150 - resetButton.getWidth() - 2);
+        } else {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), x, y + 5, 16777215);
+            this.resetButton.x = x + entryWidth - resetButton.getWidth();
+            this.sliderWidget.x = x + entryWidth - 150;
+            this.sliderWidget.setWidth(150 - resetButton.getWidth() - 2);
+        }
+        resetButton.render(mouseX, mouseY, delta);
+        sliderWidget.render(mouseX, mouseY, delta);
+    }
+    
+    private class Slider extends SliderWidget {
+        
+        protected Slider(int int_1, int int_2, int int_3, int int_4, double double_1) {
+            super(int_1, int_2, int_3, int_4, double_1);
+        }
+        
+        @Override
+        public void updateMessage() {
+            setMessage(textGetter.apply(LongSliderEntry.this.value.get()));
+        }
+        
+        @Override
+        protected void applyValue() {
+            LongSliderEntry.this.value.set((long) (minimum + Math.abs(maximum - minimum) * value));
+            getScreen().setEdited(true);
+        }
+        
+        @Override
+        public boolean keyPressed(int int_1, int int_2, int int_3) {
+            if (!isEditable())
+                return false;
+            return super.keyPressed(int_1, int_2, int_3);
+        }
+        
+        @Override
+        public boolean mouseDragged(double double_1, double double_2, int int_1, double double_3, double double_4) {
+            if (!isEditable())
+                return false;
+            return super.mouseDragged(double_1, double_2, int_1, double_3, double_4);
+        }
+        
+        public double getValue() {
+            return value;
+        }
+        
+        public void setValue(double integer) {
+            this.value = integer;
+        }
+    }
+    
+}

+ 41 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/StringListEntry.java

@@ -0,0 +1,41 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class StringListEntry extends TextFieldListEntry<String> {
+    
+    private Consumer<String> saveConsumer;
+    
+    public StringListEntry(String fieldName, String value, Consumer<String> saveConsumer) {
+        this(fieldName, value, "text.cloth-config.reset_value", null, saveConsumer);
+    }
+    
+    public StringListEntry(String fieldName, String value, String resetButtonKey, Supplier<String> defaultValue, Consumer<String> saveConsumer) {
+        super(fieldName, value, resetButtonKey, defaultValue);
+        this.saveConsumer = saveConsumer;
+    }
+    
+    public StringListEntry(String fieldName, String value, String resetButtonKey, Supplier<String> defaultValue, Consumer<String> saveConsumer, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, value, resetButtonKey, defaultValue, tooltipSupplier);
+        this.saveConsumer = saveConsumer;
+    }
+    
+    @Override
+    public String getValue() {
+        return textFieldWidget.getText();
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    @Override
+    protected boolean isMatchDefault(String text) {
+        return getDefaultValue().isPresent() ? text.equals(getDefaultValue().get()) : false;
+    }
+    
+}

+ 132 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/SubCategoryListEntry.java

@@ -0,0 +1,132 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import com.google.common.collect.Lists;
+import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.util.Identifier;
+
+import java.awt.*;
+import java.util.List;
+import java.util.Optional;
+
+public class SubCategoryListEntry extends TooltipListEntry<List<AbstractConfigListEntry>> {
+    
+    private static final Identifier CONFIG_TEX = new Identifier("cloth-config2", "textures/gui/cloth_config.png");
+    private String categoryName;
+    private List<AbstractConfigListEntry> entries;
+    private CategoryLabelWidget widget;
+    private List<Element> children;
+    private boolean expended;
+    
+    public SubCategoryListEntry(String categoryName, List<AbstractConfigListEntry> entries, boolean defaultExpended) {
+        super(categoryName, null);
+        this.categoryName = categoryName;
+        this.entries = entries;
+        this.expended = defaultExpended;
+        this.widget = new CategoryLabelWidget();
+        this.children = Lists.newArrayList(widget);
+        this.children.addAll(entries);
+    }
+    
+    public String getCategoryName() {
+        return categoryName;
+    }
+    
+    @Override
+    public List<AbstractConfigListEntry> getValue() {
+        return entries;
+    }
+    
+    @Override
+    public Optional<List<AbstractConfigListEntry>> getDefaultValue() {
+        return Optional.empty();
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        widget.rectangle.x = x - 19;
+        widget.rectangle.y = y;
+        widget.rectangle.width = entryWidth + 19;
+        widget.rectangle.height = 24;
+        MinecraftClient.getInstance().getTextureManager().bindTexture(CONFIG_TEX);
+        GuiLighting.disable();
+        GlStateManager.color4f(1, 1, 1, 1);
+        blit(x - 15, y + 4, 24, expended ? 9 : 0, 9, 9);
+        MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(categoryName), x, y + 5, -1);
+        for(AbstractConfigListEntry entry : entries) {
+            entry.setParent(getParent());
+            entry.setScreen(getScreen());
+        }
+        if (expended) {
+            int yy = y + 24;
+            for(AbstractConfigListEntry entry : entries) {
+                entry.render(-1, yy, x + 14, entryWidth - 14, entry.getItemHeight(), mouseX, mouseY, isSelected, delta);
+                yy += entry.getItemHeight();
+            }
+        }
+    }
+    
+    @Override
+    public boolean isMouseInside(int mouseX, int mouseY, int x, int y, int entryWidth, int entryHeight) {
+        widget.rectangle.x = x - 15;
+        widget.rectangle.y = y;
+        widget.rectangle.width = entryWidth + 15;
+        widget.rectangle.height = 24;
+        return widget.rectangle.contains(mouseX, mouseY) && getParent().isMouseOver(mouseX, mouseY);
+    }
+    
+    @Override
+    public int getItemHeight() {
+        if (expended) {
+            int i = 24;
+            for(AbstractConfigListEntry entry : entries)
+                i += entry.getItemHeight();
+            return i;
+        }
+        return 24;
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return children;
+    }
+    
+    @Override
+    public void save() {
+        entries.forEach(AbstractConfigListEntry::save);
+    }
+    
+    @Override
+    public Optional<String> getError() {
+        String error = null;
+        for(AbstractConfigListEntry entry : entries)
+            if (entry.getError().isPresent()) {
+                if (error != null)
+                    return Optional.ofNullable(I18n.translate("text.cloth-config.multi_error"));
+                return Optional.ofNullable((String) entry.getError().get());
+            }
+        return Optional.ofNullable(error);
+    }
+    
+    public class CategoryLabelWidget implements Element {
+        private Rectangle rectangle = new Rectangle();
+        
+        @Override
+        public boolean mouseClicked(double double_1, double double_2, int int_1) {
+            if (rectangle.contains(double_1, double_2)) {
+                expended = !expended;
+                MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                return true;
+            }
+            return false;
+        }
+    }
+    
+}

+ 106 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/TextFieldListEntry.java

@@ -0,0 +1,106 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import com.google.common.collect.Lists;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.Window;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+public abstract class TextFieldListEntry<T> extends TooltipListEntry<T> {
+    
+    protected TextFieldWidget textFieldWidget;
+    protected ButtonWidget resetButton;
+    protected Supplier<T> defaultValue;
+    protected T original;
+    protected List<Element> widgets;
+    
+    protected TextFieldListEntry(String fieldName, T original, String resetButtonKey, Supplier<T> defaultValue) {
+        this(fieldName, original, resetButtonKey, defaultValue, null);
+    }
+    
+    protected TextFieldListEntry(String fieldName, T original, String resetButtonKey, Supplier<T> defaultValue, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, tooltipSupplier);
+        this.defaultValue = defaultValue;
+        this.original = original;
+        this.textFieldWidget = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 148, 18, "") {
+            @Override
+            public void render(int int_1, int int_2, float float_1) {
+                boolean f = isFocused();
+                setFocused(TextFieldListEntry.this.getParent().getFocused() == TextFieldListEntry.this && TextFieldListEntry.this.getFocused() == this);
+                textFieldPreRender(this);
+                super.render(int_1, int_2, float_1);
+                setFocused(f);
+            }
+            
+            @Override
+            public void addText(String string_1) {
+                super.addText(stripAddText(string_1));
+            }
+        };
+        textFieldWidget.setMaxLength(999999);
+        textFieldWidget.setText(String.valueOf(original));
+        textFieldWidget.setChangedListener(s -> {
+            if (!original.equals(s))
+                getScreen().setEdited(true);
+        });
+        this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(resetButtonKey)) + 6, 20, I18n.translate(resetButtonKey), widget -> {
+            TextFieldListEntry.this.textFieldWidget.setText(String.valueOf(defaultValue.get()));
+            getScreen().setEdited(true);
+        });
+        this.widgets = Lists.newArrayList(textFieldWidget, resetButton);
+    }
+    
+    protected static void setTextFieldWidth(TextFieldWidget widget, int width) {
+        widget.setWidth(width);
+    }
+    
+    protected String stripAddText(String s) {
+        return s;
+    }
+    
+    protected void textFieldPreRender(TextFieldWidget widget) {
+    
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        Window window = MinecraftClient.getInstance().window;
+        this.resetButton.active = isEditable() && getDefaultValue().isPresent() && !isMatchDefault(textFieldWidget.getText());
+        this.resetButton.y = y;
+        this.textFieldWidget.setIsEditable(isEditable());
+        this.textFieldWidget.y = y + 1;
+        if (MinecraftClient.getInstance().textRenderer.isRightToLeft()) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), window.getScaledWidth() - x - MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(getFieldName())), y + 5, 16777215);
+            this.resetButton.x = x;
+            this.textFieldWidget.x = x + resetButton.getWidth();
+            setTextFieldWidth(textFieldWidget, 148 - resetButton.getWidth() - 4);
+        } else {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), x, y + 5, 16777215);
+            this.resetButton.x = x + entryWidth - resetButton.getWidth();
+            this.textFieldWidget.x = x + entryWidth - 148;
+            setTextFieldWidth(textFieldWidget, 148 - resetButton.getWidth() - 4);
+        }
+        resetButton.render(mouseX, mouseY, delta);
+        textFieldWidget.render(mouseX, mouseY, delta);
+    }
+    
+    protected abstract boolean isMatchDefault(String text);
+    
+    @Override
+    public Optional<T> getDefaultValue() {
+        return defaultValue == null ? Optional.empty() : Optional.ofNullable(defaultValue.get());
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return widgets;
+    }
+    
+}

+ 73 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/TextListEntry.java

@@ -0,0 +1,73 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+public class TextListEntry extends TooltipListEntry {
+    
+    private int savedWidth = -1;
+    private int color;
+    private String text;
+    
+    public TextListEntry(String fieldName, String text) {
+        this(fieldName, text, -1);
+    }
+    
+    public TextListEntry(String fieldName, String text, int color) {
+        this(fieldName, text, color, null);
+    }
+    
+    public TextListEntry(String fieldName, String text, int color, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName, tooltipSupplier);
+        this.text = text;
+        this.color = color;
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        this.savedWidth = entryWidth;
+        int yy = y + 4;
+        List<String> strings = MinecraftClient.getInstance().textRenderer.wrapStringToWidthAsList(text, savedWidth);
+        for(int i = 0; i < strings.size(); i++) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(strings.get(i), x, yy, color);
+            yy += MinecraftClient.getInstance().textRenderer.fontHeight + 3;
+        }
+    }
+    
+    @Override
+    public int getItemHeight() {
+        if (savedWidth == -1)
+            return 12;
+        List<String> strings = MinecraftClient.getInstance().textRenderer.wrapStringToWidthAsList(text, savedWidth);
+        if (strings.isEmpty())
+            return 0;
+        return 15 + strings.size() * 12;
+    }
+    
+    @Override
+    public void save() {
+    
+    }
+    
+    @Override
+    public Object getValue() {
+        return null;
+    }
+    
+    @Override
+    public Optional<Object> getDefaultValue() {
+        return Optional.empty();
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+    
+}

+ 46 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/TooltipListEntry.java

@@ -0,0 +1,46 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.api.QueuedTooltip;
+
+import java.awt.*;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+public abstract class TooltipListEntry<T> extends AbstractConfigListEntry<T> {
+    
+    private Supplier<Optional<String[]>> tooltipSupplier;
+    
+    public TooltipListEntry(String fieldName, Supplier<Optional<String[]>> tooltipSupplier) {
+        super(fieldName);
+        this.tooltipSupplier = tooltipSupplier;
+    }
+    
+    @Override
+    public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        if (isMouseInside(mouseX, mouseY, x, y, entryWidth, entryHeight)) {
+            Optional<String[]> tooltip = getTooltip();
+            if (tooltip.isPresent() && tooltip.get().length > 0)
+                getScreen().queueTooltip(QueuedTooltip.create(new Point(mouseX, mouseY), tooltip.get()));
+        }
+    }
+    
+    public boolean isMouseInside(int mouseX, int mouseY, int x, int y, int entryWidth, int entryHeight) {
+        return mouseX >= x && mouseY >= y && mouseX <= x + entryWidth && mouseY <= y + entryHeight && getParent().isMouseOver(mouseX, mouseY);
+    }
+    
+    public Optional<String[]> getTooltip() {
+        if (tooltipSupplier != null)
+            return tooltipSupplier.get();
+        return Optional.empty();
+    }
+    
+    public Supplier<Optional<String[]>> getTooltipSupplier() {
+        return tooltipSupplier;
+    }
+    
+    public void setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+    }
+    
+}

+ 53 - 0
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicElementListWidget.java

@@ -0,0 +1,53 @@
+package me.shedaniel.clothconfig2.gui.widget;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.ParentElement;
+import net.minecraft.util.Identifier;
+
+@Environment(EnvType.CLIENT)
+public abstract class DynamicElementListWidget<E extends DynamicElementListWidget.ElementEntry<E>> extends DynamicSmoothScrollingEntryListWidget<E> {
+    
+    public DynamicElementListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+        super(client, width, height, top, bottom, backgroundLocation);
+    }
+    
+    public boolean changeFocus(boolean boolean_1) {
+        boolean boolean_2 = super.changeFocus(boolean_1);
+        if (boolean_2)
+            this.ensureVisible(this.getFocused());
+        return boolean_2;
+    }
+    
+    protected boolean isSelected(int int_1) {
+        return false;
+    }
+    
+    @Environment(EnvType.CLIENT)
+    public abstract static class ElementEntry<E extends ElementEntry<E>> extends Entry<E> implements ParentElement {
+        private Element focused;
+        private boolean dragging;
+        
+        public ElementEntry() {
+        }
+        
+        public boolean isDragging() {
+            return this.dragging;
+        }
+        
+        public void setDragging(boolean boolean_1) {
+            this.dragging = boolean_1;
+        }
+        
+        public Element getFocused() {
+            return this.focused;
+        }
+        
+        public void setFocused(Element element_1) {
+            this.focused = element_1;
+        }
+    }
+}
+

+ 520 - 0
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicEntryListWidget.java

@@ -0,0 +1,520 @@
+package me.shedaniel.clothconfig2.gui.widget;
+
+import com.google.common.collect.Lists;
+import com.mojang.blaze3d.platform.GlStateManager;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.AbstractParentElement;
+import net.minecraft.client.gui.Drawable;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.AbstractList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Environment(EnvType.CLIENT)
+public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.Entry<E>> extends AbstractParentElement implements Drawable {
+    protected static final int DRAG_OUTSIDE = -2;
+    protected final MinecraftClient client;
+    private final List<E> entries = new Entries();
+    protected int width;
+    protected int height;
+    protected int top;
+    protected int bottom;
+    protected int right;
+    protected int left;
+    protected boolean verticallyCenter = true;
+    protected int yDrag = -2;
+    protected boolean visible = true;
+    protected boolean renderSelection;
+    protected int headerHeight;
+    protected double scroll;
+    protected boolean scrolling;
+    protected E selectedItem;
+    protected Identifier backgroundLocation;
+    
+    public DynamicEntryListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+        this.client = client;
+        this.width = width;
+        this.height = height;
+        this.top = top;
+        this.bottom = bottom;
+        this.left = 0;
+        this.right = width;
+        this.backgroundLocation = backgroundLocation;
+    }
+    
+    public void setRenderSelection(boolean boolean_1) {
+        this.visible = boolean_1;
+    }
+    
+    protected void setRenderHeader(boolean boolean_1, int headerHeight) {
+        this.renderSelection = boolean_1;
+        this.headerHeight = headerHeight;
+        if (!boolean_1)
+            this.headerHeight = 0;
+    }
+    
+    public int getItemWidth() {
+        return 220;
+    }
+    
+    public E getSelectedItem() {
+        return this.selectedItem;
+    }
+    
+    public void selectItem(E item) {
+        this.selectedItem = item;
+    }
+    
+    public E getFocused() {
+        return (E) super.getFocused();
+    }
+    
+    public final List<E> children() {
+        return this.entries;
+    }
+    
+    protected final void clearItems() {
+        this.entries.clear();
+    }
+    
+    protected E getItem(int index) {
+        return (E) this.children().get(index);
+    }
+    
+    protected int addItem(E item) {
+        this.entries.add(item);
+        return this.entries.size() - 1;
+    }
+    
+    protected int getItemCount() {
+        return this.children().size();
+    }
+    
+    protected boolean isSelected(int index) {
+        return Objects.equals(this.getSelectedItem(), this.children().get(index));
+    }
+    
+    protected final E getItemAtPosition(double mouseX, double mouseY) {
+        int listMiddleX = this.left + this.width / 2;
+        int minX = listMiddleX - this.getItemWidth() / 2;
+        int maxX = listMiddleX + this.getItemWidth() / 2;
+        int currentY = MathHelper.floor(mouseY - (double) this.top) - this.headerHeight + (int) this.getScroll() - 4;
+        int itemY = 0;
+        int itemIndex = -1;
+        for(int i = 0; i < entries.size(); i++) {
+            E item = getItem(i);
+            itemY += item.getItemHeight();
+            if (itemY > currentY) {
+                itemIndex = i;
+                break;
+            }
+        }
+        return mouseX < (double) this.getScrollbarPosition() && mouseX >= minX && mouseX <= maxX && itemIndex >= 0 && currentY >= 0 && itemIndex < this.getItemCount() ? (E) this.children().get(itemIndex) : null;
+    }
+    
+    public void updateSize(int width, int height, int top, int bottom) {
+        this.width = width;
+        this.height = height;
+        this.top = top;
+        this.bottom = bottom;
+        this.left = 0;
+        this.right = width;
+    }
+    
+    public void setLeftPos(int left) {
+        this.left = left;
+        this.right = left + this.width;
+    }
+    
+    protected int getMaxScrollPosition() {
+        AtomicInteger integer = new AtomicInteger(headerHeight);
+        entries.forEach(item -> integer.addAndGet(item.getItemHeight()));
+        return integer.get();
+    }
+    
+    protected void clickedHeader(int int_1, int int_2) {
+    }
+    
+    protected void renderHeader(int int_1, int int_2, Tessellator tessellator) {
+    }
+    
+    protected void drawBackground() {
+    }
+    
+    protected void renderDecorations(int int_1, int int_2) {
+    }
+    
+    public void render(int mouseX, int mouseY, float delta) {
+        this.drawBackground();
+        int scrollbarPosition = this.getScrollbarPosition();
+        int int_4 = scrollbarPosition + 6;
+        GlStateManager.disableLighting();
+        GlStateManager.disableFog();
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder buffer = tessellator.getBufferBuilder();
+        this.client.getTextureManager().bindTexture(backgroundLocation);
+        GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        float float_2 = 32.0F;
+        buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+        buffer.vertex(this.left, this.bottom, 0.0D).texture(this.left / 32.0F, ((this.bottom + (int) this.getScroll()) / 32.0F)).color(32, 32, 32, 255).next();
+        buffer.vertex(this.right, this.bottom, 0.0D).texture(this.right / 32.0F, ((this.bottom + (int) this.getScroll()) / 32.0F)).color(32, 32, 32, 255).next();
+        buffer.vertex(this.right, this.top, 0.0D).texture(this.right / 32.0F, ((this.top + (int) this.getScroll()) / 32.0F)).color(32, 32, 32, 255).next();
+        buffer.vertex(this.left, this.top, 0.0D).texture(this.left / 32.0F, ((this.top + (int) this.getScroll()) / 32.0F)).color(32, 32, 32, 255).next();
+        tessellator.draw();
+        int rowLeft = this.getRowLeft();
+        int startY = this.top + 4 - (int) this.getScroll();
+        if (this.renderSelection)
+            this.renderHeader(rowLeft, startY, tessellator);
+        
+        this.renderList(rowLeft, startY, mouseX, mouseY, delta);
+        GlStateManager.disableDepthTest();
+        this.renderHoleBackground(0, this.top, 255, 255);
+        this.renderHoleBackground(this.bottom, this.height, 255, 255);
+        GlStateManager.enableBlend();
+        GlStateManager.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE);
+        GlStateManager.disableAlphaTest();
+        GlStateManager.shadeModel(7425);
+        GlStateManager.disableTexture();
+        buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+        buffer.vertex(this.left, this.top + 4, 0.0D).texture(0.0D, 1.0D).color(0, 0, 0, 0).next();
+        buffer.vertex(this.right, this.top + 4, 0.0D).texture(1.0D, 1.0D).color(0, 0, 0, 0).next();
+        buffer.vertex(this.right, this.top, 0.0D).texture(1.0D, 0.0D).color(0, 0, 0, 255).next();
+        buffer.vertex(this.left, this.top, 0.0D).texture(0.0D, 0.0D).color(0, 0, 0, 255).next();
+        tessellator.draw();
+        buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+        buffer.vertex(this.left, this.bottom, 0.0D).texture(0.0D, 1.0D).color(0, 0, 0, 255).next();
+        buffer.vertex(this.right, this.bottom, 0.0D).texture(1.0D, 1.0D).color(0, 0, 0, 255).next();
+        buffer.vertex(this.right, this.bottom - 4, 0.0D).texture(1.0D, 0.0D).color(0, 0, 0, 0).next();
+        buffer.vertex(this.left, this.bottom - 4, 0.0D).texture(0.0D, 0.0D).color(0, 0, 0, 0).next();
+        tessellator.draw();
+        int maxScroll = this.getMaxScroll();
+        renderScrollBar(tessellator, buffer, maxScroll, scrollbarPosition, int_4);
+        
+        this.renderDecorations(mouseX, mouseY);
+        GlStateManager.enableTexture();
+        GlStateManager.shadeModel(7424);
+        GlStateManager.enableAlphaTest();
+        GlStateManager.disableBlend();
+    }
+    
+    protected void renderScrollBar(Tessellator tessellator, BufferBuilder buffer, int maxScroll, int scrollbarPositionMinX, int scrollbarPositionMaxX) {
+        if (maxScroll > 0) {
+            int int_9 = (int) (((this.bottom - this.top) * (this.bottom - this.top)) / this.getMaxScrollPosition());
+            int_9 = MathHelper.clamp(int_9, 32, this.bottom - this.top - 8);
+            int int_10 = (int) this.getScroll() * (this.bottom - this.top - int_9) / maxScroll + this.top;
+            if (int_10 < this.top) {
+                int_10 = this.top;
+            }
+            
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, this.bottom, 0.0D).texture(0.0D, 1.0D).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, this.bottom, 0.0D).texture(1.0D, 1.0D).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, this.top, 0.0D).texture(1.0D, 0.0D).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMinX, this.top, 0.0D).texture(0.0D, 0.0D).color(0, 0, 0, 255).next();
+            tessellator.draw();
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, int_10 + int_9, 0.0D).texture(0.0D, 1.0D).color(128, 128, 128, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, int_10 + int_9, 0.0D).texture(1.0D, 1.0D).color(128, 128, 128, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, int_10, 0.0D).texture(1.0D, 0.0D).color(128, 128, 128, 255).next();
+            buffer.vertex(scrollbarPositionMinX, int_10, 0.0D).texture(0.0D, 0.0D).color(128, 128, 128, 255).next();
+            tessellator.draw();
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, (int_10 + int_9 - 1), 0.0D).texture(0.0D, 1.0D).color(192, 192, 192, 255).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), (int_10 + int_9 - 1), 0.0D).texture(1.0D, 1.0D).color(192, 192, 192, 255).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), int_10, 0.0D).texture(1.0D, 0.0D).color(192, 192, 192, 255).next();
+            buffer.vertex(scrollbarPositionMinX, int_10, 0.0D).texture(0.0D, 0.0D).color(192, 192, 192, 255).next();
+            tessellator.draw();
+        }
+    }
+    
+    protected void centerScrollOn(E item) {
+        double d = (this.bottom - this.top) / -2;
+        for(int i = 0; i < this.children().indexOf(item) && i < this.getItemCount(); i++)
+            d += getItem(i).getItemHeight();
+        this.capYPosition(d);
+    }
+    
+    protected void ensureVisible(E item) {
+        int rowTop = this.getRowTop(this.children().indexOf(item));
+        int int_2 = rowTop - this.top - 4 - item.getItemHeight();
+        if (int_2 < 0)
+            this.scroll(int_2);
+        int int_3 = this.bottom - rowTop - item.getItemHeight() * 2;
+        if (int_3 < 0)
+            this.scroll(-int_3);
+    }
+    
+    protected void scroll(int int_1) {
+        this.capYPosition(this.getScroll() + (double) int_1);
+        this.yDrag = -2;
+    }
+    
+    public double getScroll() {
+        return this.scroll;
+    }
+    
+    public void capYPosition(double double_1) {
+        this.scroll = MathHelper.clamp(double_1, 0.0D, (double) this.getMaxScroll());
+    }
+    
+    protected int getMaxScroll() {
+        return Math.max(0, this.getMaxScrollPosition() - (this.bottom - this.top - 4));
+    }
+    
+    public int getScrollBottom() {
+        return (int) this.getScroll() - this.height - this.headerHeight;
+    }
+    
+    protected void updateScrollingState(double double_1, double double_2, int int_1) {
+        this.scrolling = int_1 == 0 && double_1 >= (double) this.getScrollbarPosition() && double_1 < (double) (this.getScrollbarPosition() + 6);
+    }
+    
+    protected int getScrollbarPosition() {
+        return this.width / 2 + 124;
+    }
+    
+    public boolean mouseClicked(double double_1, double double_2, int int_1) {
+        this.updateScrollingState(double_1, double_2, int_1);
+        if (!this.isMouseOver(double_1, double_2)) {
+            return false;
+        } else {
+            E item = this.getItemAtPosition(double_1, double_2);
+            if (item != null) {
+                if (item.mouseClicked(double_1, double_2, int_1)) {
+                    this.setFocused(item);
+                    this.setDragging(true);
+                    return true;
+                }
+            } else if (int_1 == 0) {
+                this.clickedHeader((int) (double_1 - (double) (this.left + this.width / 2 - this.getItemWidth() / 2)), (int) (double_2 - (double) this.top) + (int) this.getScroll() - 4);
+                return true;
+            }
+            
+            return this.scrolling;
+        }
+    }
+    
+    public boolean mouseReleased(double double_1, double double_2, int int_1) {
+        if (this.getFocused() != null) {
+            this.getFocused().mouseReleased(double_1, double_2, int_1);
+        }
+        
+        return false;
+    }
+    
+    public boolean mouseDragged(double double_1, double double_2, int int_1, double double_3, double double_4) {
+        if (super.mouseDragged(double_1, double_2, int_1, double_3, double_4)) {
+            return true;
+        } else if (int_1 == 0 && this.scrolling) {
+            if (double_2 < (double) this.top) {
+                this.capYPosition(0.0D);
+            } else if (double_2 > (double) this.bottom) {
+                this.capYPosition((double) this.getMaxScroll());
+            } else {
+                double double_5 = (double) Math.max(1, this.getMaxScroll());
+                int int_2 = this.bottom - this.top;
+                int int_3 = MathHelper.clamp((int) ((float) (int_2 * int_2) / (float) this.getMaxScrollPosition()), 32, int_2 - 8);
+                double double_6 = Math.max(1.0D, double_5 / (double) (int_2 - int_3));
+                this.capYPosition(this.getScroll() + double_4 * double_6);
+            }
+            
+            return true;
+        } else {
+            return false;
+        }
+    }
+    
+    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        this.capYPosition(this.getScroll() - double_3 * (double) (getMaxScroll() / getItemCount()) / 2.0D);
+        return true;
+    }
+    
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if (super.keyPressed(int_1, int_2, int_3)) {
+            return true;
+        } else if (int_1 == 264) {
+            this.moveSelection(1);
+            return true;
+        } else if (int_1 == 265) {
+            this.moveSelection(-1);
+            return true;
+        } else {
+            return false;
+        }
+    }
+    
+    protected void moveSelection(int int_1) {
+        if (!this.children().isEmpty()) {
+            int int_2 = this.children().indexOf(this.getSelectedItem());
+            int int_3 = MathHelper.clamp(int_2 + int_1, 0, this.getItemCount() - 1);
+            E itemListWidget$Item_1 = (E) this.children().get(int_3);
+            this.selectItem(itemListWidget$Item_1);
+            this.ensureVisible(itemListWidget$Item_1);
+        }
+        
+    }
+    
+    public boolean isMouseOver(double double_1, double double_2) {
+        return double_2 >= (double) this.top && double_2 <= (double) this.bottom && double_1 >= (double) this.left && double_1 <= (double) this.right;
+    }
+    
+    protected void renderList(int startX, int startY, int int_3, int int_4, float float_1) {
+        int itemCount = this.getItemCount();
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder buffer = tessellator.getBufferBuilder();
+        
+        for(int renderIndex = 0; renderIndex < itemCount; ++renderIndex) {
+            E item = this.getItem(renderIndex);
+            int itemY = startY + headerHeight;
+            for(int i = 0; i < entries.size() && i < renderIndex; i++)
+                itemY += entries.get(i).getItemHeight();
+            int itemHeight = item.getItemHeight() - 4;
+            int itemWidth = this.getItemWidth();
+            int itemMinX, itemMaxX;
+            if (this.visible && this.isSelected(renderIndex)) {
+                itemMinX = this.left + this.width / 2 - itemWidth / 2;
+                itemMaxX = itemMinX + itemWidth;
+                GlStateManager.disableTexture();
+                float float_2 = this.isFocused() ? 1.0F : 0.5F;
+                GlStateManager.color4f(float_2, float_2, float_2, 1.0F);
+                buffer.begin(7, VertexFormats.POSITION);
+                buffer.vertex((double) itemMinX, (double) (itemY + itemHeight + 2), 0.0D).next();
+                buffer.vertex((double) itemMaxX, (double) (itemY + itemHeight + 2), 0.0D).next();
+                buffer.vertex((double) itemMaxX, (double) (itemY - 2), 0.0D).next();
+                buffer.vertex((double) itemMinX, (double) (itemY - 2), 0.0D).next();
+                tessellator.draw();
+                GlStateManager.color4f(0.0F, 0.0F, 0.0F, 1.0F);
+                buffer.begin(7, VertexFormats.POSITION);
+                buffer.vertex((double) (itemMinX + 1), (double) (itemY + itemHeight + 1), 0.0D).next();
+                buffer.vertex((double) (itemMaxX - 1), (double) (itemY + itemHeight + 1), 0.0D).next();
+                buffer.vertex((double) (itemMaxX - 1), (double) (itemY - 1), 0.0D).next();
+                buffer.vertex((double) (itemMinX + 1), (double) (itemY - 1), 0.0D).next();
+                tessellator.draw();
+                GlStateManager.enableTexture();
+            }
+            
+            int y = this.getRowTop(renderIndex);
+            int x = this.getRowLeft();
+            GuiLighting.disable();
+            item.render(renderIndex, y, x, itemWidth, itemHeight, int_3, int_4, this.isMouseOver((double) int_3, (double) int_4) && Objects.equals(this.getItemAtPosition((double) int_3, (double) int_4), item), float_1);
+        }
+        
+    }
+    
+    protected int getRowLeft() {
+        return this.left + this.width / 2 - this.getItemWidth() / 2 + 2;
+    }
+    
+    protected int getRowTop(int index) {
+        int integer = top + 4 - (int) this.getScroll() + headerHeight;
+        for(int i = 0; i < entries.size() && i < index; i++)
+            integer += entries.get(i).getItemHeight();
+        return integer;
+    }
+    
+    protected boolean isFocused() {
+        return false;
+    }
+    
+    protected void renderHoleBackground(int int_1, int int_2, int int_3, int int_4) {
+        Tessellator tessellator_1 = Tessellator.getInstance();
+        BufferBuilder bufferBuilder_1 = tessellator_1.getBufferBuilder();
+        this.client.getTextureManager().bindTexture(backgroundLocation);
+        GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        float float_1 = 32.0F;
+        bufferBuilder_1.begin(7, VertexFormats.POSITION_UV_COLOR);
+        bufferBuilder_1.vertex((double) this.left, (double) int_2, 0.0D).texture(0.0D, (double) ((float) int_2 / 32.0F)).color(64, 64, 64, int_4).next();
+        bufferBuilder_1.vertex((double) (this.left + this.width), (double) int_2, 0.0D).texture((double) ((float) this.width / 32.0F), (double) ((float) int_2 / 32.0F)).color(64, 64, 64, int_4).next();
+        bufferBuilder_1.vertex((double) (this.left + this.width), (double) int_1, 0.0D).texture((double) ((float) this.width / 32.0F), (double) ((float) int_1 / 32.0F)).color(64, 64, 64, int_3).next();
+        bufferBuilder_1.vertex((double) this.left, (double) int_1, 0.0D).texture(0.0D, (double) ((float) int_1 / 32.0F)).color(64, 64, 64, int_3).next();
+        tessellator_1.draw();
+    }
+    
+    protected E remove(int int_1) {
+        E itemListWidget$Item_1 = (E) this.entries.get(int_1);
+        return this.removeEntry((E) this.entries.get(int_1)) ? itemListWidget$Item_1 : null;
+    }
+    
+    protected boolean removeEntry(E itemListWidget$Item_1) {
+        boolean boolean_1 = this.entries.remove(itemListWidget$Item_1);
+        if (boolean_1 && itemListWidget$Item_1 == this.getSelectedItem()) {
+            this.selectItem((E) null);
+        }
+        
+        return boolean_1;
+    }
+    
+    @Environment(EnvType.CLIENT)
+    public abstract static class Entry<E extends Entry<E>> extends DrawableHelper implements Element {
+        @Deprecated
+        DynamicEntryListWidget<E> parent;
+        
+        public Entry() {
+        }
+        
+        public abstract void render(int var1, int var2, int var3, int var4, int var5, int var6, int var7, boolean var8, float var9);
+        
+        public boolean isMouseOver(double double_1, double double_2) {
+            return Objects.equals(this.parent.getItemAtPosition(double_1, double_2), this);
+        }
+        
+        public DynamicEntryListWidget<E> getParent() {
+            return parent;
+        }
+        
+        public void setParent(DynamicEntryListWidget<E> parent) {
+            this.parent = parent;
+        }
+        
+        public abstract int getItemHeight();
+    }
+    
+    @Environment(EnvType.CLIENT)
+    class Entries extends AbstractList<E> {
+        private final List<E> items;
+        
+        private Entries() {
+            this.items = Lists.newArrayList();
+        }
+        
+        @Override
+        public E get(int int_1) {
+            return (E) this.items.get(int_1);
+        }
+        
+        @Override
+        public int size() {
+            return this.items.size();
+        }
+        
+        @Override
+        public E set(int int_1, E itemListWidget$Item_1) {
+            E itemListWidget$Item_2 = (E) this.items.set(int_1, itemListWidget$Item_1);
+            itemListWidget$Item_1.parent = DynamicEntryListWidget.this;
+            return itemListWidget$Item_2;
+        }
+        
+        @Override
+        public void add(int int_1, E itemListWidget$Item_1) {
+            this.items.add(int_1, itemListWidget$Item_1);
+            itemListWidget$Item_1.parent = DynamicEntryListWidget.this;
+        }
+        
+        @Override
+        public E remove(int int_1) {
+            return (E) this.items.remove(int_1);
+        }
+    }
+}
+

+ 168 - 0
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicSmoothScrollingEntryListWidget.java

@@ -0,0 +1,168 @@
+package me.shedaniel.clothconfig2.gui.widget;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public abstract class DynamicSmoothScrollingEntryListWidget<E extends DynamicEntryListWidget.Entry<E>> extends DynamicEntryListWidget<E> {
+    
+    private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
+    
+    static {
+        EXECUTOR_SERVICE.scheduleWithFixedDelay(() -> {
+            if (MinecraftClient.getInstance() != null && MinecraftClient.getInstance().currentScreen != null)
+                for(Element child : MinecraftClient.getInstance().currentScreen.children())
+                    if (child instanceof DynamicSmoothScrollingEntryListWidget)
+                        ((DynamicSmoothScrollingEntryListWidget) child).updateScrolling();
+        }, 0, 1000 / 60, TimeUnit.MILLISECONDS);
+    }
+    
+    protected double scrollVelocity;
+    protected boolean smoothScrolling = true;
+    
+    public DynamicSmoothScrollingEntryListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+        super(client, width, height, top, bottom, backgroundLocation);
+    }
+    
+    public double getScrollVelocity() {
+        return scrollVelocity;
+    }
+    
+    public void setScrollVelocity(double scrollVelocity) {
+        this.scrollVelocity = scrollVelocity;
+    }
+    
+    public boolean isSmoothScrolling() {
+        return smoothScrolling;
+    }
+    
+    public void setSmoothScrolling(boolean smoothScrolling) {
+        this.smoothScrolling = smoothScrolling;
+    }
+    
+    @Override
+    public void capYPosition(double double_1) {
+        if (smoothScrolling)
+            this.scroll = double_1;
+        else
+            this.scroll = MathHelper.clamp(double_1, 0.0D, (double) this.getMaxScroll());
+    }
+    
+    public void updateScrolling() {
+        if (smoothScrolling) {
+            double change = scrollVelocity * 0.3d;
+            if (scrollVelocity != 0) {
+                scroll += change;
+                scrollVelocity -= scrollVelocity * (scroll >= 0 && scroll <= getMaxScroll() ? 0.2d : .4d);
+                if (Math.abs(scrollVelocity) < .1)
+                    scrollVelocity = 0d;
+            }
+            if (scroll < 0d && scrollVelocity == 0d) {
+                scroll = Math.min(scroll + (0 - scroll) * 0.2d, 0);
+                if (scroll > -0.1d && scroll < 0d)
+                    scroll = 0d;
+            } else if (scroll > getMaxScroll() && scrollVelocity == 0d) {
+                scroll = Math.max(scroll - (scroll - getMaxScroll()) * 0.2d, getMaxScroll());
+                if (scroll > getMaxScroll() && scroll < getMaxScroll() + 0.1d)
+                    scroll = getMaxScroll();
+            }
+        } else {
+            scroll += scrollVelocity;
+            scrollVelocity = 0d;
+            capYPosition(scroll);
+        }
+    }
+    
+    @Override
+    public boolean mouseDragged(double double_1, double double_2, int int_1, double double_3, double double_4) {
+        if (!smoothScrolling)
+            return super.mouseDragged(double_1, double_2, int_1, double_3, double_4);
+        if (this.getFocused() != null && this.isDragging() && int_1 == 0 ? this.getFocused().mouseDragged(double_1, double_2, int_1, double_3, double_4) : false) {
+            return true;
+        } else if (int_1 == 0 && this.scrolling) {
+            if (double_2 < (double) this.top) {
+                this.capYPosition(0.0D);
+            } else if (double_2 > (double) this.bottom) {
+                this.capYPosition((double) this.getMaxScroll());
+            } else {
+                double double_5 = (double) Math.max(1, this.getMaxScroll());
+                int int_2 = this.bottom - this.top;
+                int int_3 = MathHelper.clamp((int) ((float) (int_2 * int_2) / (float) this.getMaxScrollPosition()), 32, int_2 - 8);
+                double double_6 = Math.max(1.0D, double_5 / (double) (int_2 - int_3));
+                this.capYPosition(MathHelper.clamp(this.getScroll() + double_4 * double_6, 0, getMaxScroll()));
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    protected void scroll(int int_1) {
+        super.scroll(int_1);
+        this.scrollVelocity = 0d;
+    }
+    
+    @Override
+    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        if (!smoothScrolling) {
+            if (double_3 < 0)
+                scrollVelocity += 16;
+            if (double_3 > 0)
+                scrollVelocity -= 16;
+            return true;
+        }
+        if (scroll >= 0 && scroll <= getMaxScroll()) {
+            if (double_3 < 0)
+                scrollVelocity += 16;
+            if (double_3 > 0)
+                scrollVelocity -= 16;
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    protected void renderScrollBar(Tessellator tessellator, BufferBuilder buffer, int maxScroll, int scrollbarPositionMinX, int scrollbarPositionMaxX) {
+        if (!smoothScrolling)
+            super.renderScrollBar(tessellator, buffer, maxScroll, scrollbarPositionMinX, scrollbarPositionMaxX);
+        else if (maxScroll > 0) {
+            int height = (int) (((this.bottom - this.top) * (this.bottom - this.top)) / this.getMaxScrollPosition());
+            height = MathHelper.clamp(height, 32, this.bottom - this.top - 8);
+            height -= Math.min((scroll < 0 ? (int) -scroll : scroll > getMaxScroll() ? (int) scroll - getMaxScroll() : 0), height * .75);
+            int minY = Math.min(Math.max((int) this.getScroll() * (this.bottom - this.top - height) / maxScroll + this.top, this.top), this.bottom - height);
+            
+            // Black Bar
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, this.bottom, 0.0D).texture(0.0D, 1.0D).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, this.bottom, 0.0D).texture(1.0D, 1.0D).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, this.top, 0.0D).texture(1.0D, 0.0D).color(0, 0, 0, 255).next();
+            buffer.vertex(scrollbarPositionMinX, this.top, 0.0D).texture(0.0D, 0.0D).color(0, 0, 0, 255).next();
+            tessellator.draw();
+            
+            // Top
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).texture(0.0D, 1.0D).color(128, 128, 128, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).texture(1.0D, 1.0D).color(128, 128, 128, 255).next();
+            buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).texture(1.0D, 0.0D).color(128, 128, 128, 255).next();
+            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0.0D, 0.0D).color(128, 128, 128, 255).next();
+            tessellator.draw();
+            
+            // Bottom
+            buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+            buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).texture(0.0D, 1.0D).color(192, 192, 192, 255).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).texture(1.0D, 1.0D).color(192, 192, 192, 255).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).texture(1.0D, 0.0D).color(192, 192, 192, 255).next();
+            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0.0D, 0.0D).color(192, 192, 192, 255).next();
+            tessellator.draw();
+        }
+    }
+    
+}

+ 200 - 0
src/main/java/me/shedaniel/clothconfig2/impl/ConfigBuilderImpl.java

@@ -0,0 +1,200 @@
+package me.shedaniel.clothconfig2.impl;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import me.shedaniel.clothconfig2.api.ConfigBuilder;
+import me.shedaniel.clothconfig2.api.ConfigCategory;
+import me.shedaniel.clothconfig2.gui.ClothConfigScreen;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class ConfigBuilderImpl implements ConfigBuilder {
+    
+    private Runnable savingRunnable;
+    private Screen parent;
+    private String title = "text.cloth-config.config";
+    private boolean editable = true;
+    private boolean tabsSmoothScroll = true;
+    private boolean listSmoothScroll = true;
+    private boolean doesProcessErrors = true;
+    private boolean doesConfirmSave = true;
+    private Identifier defaultBackground = DrawableHelper.BACKGROUND_LOCATION;
+    private Consumer<Screen> afterInitConsumer = screen -> {};
+    private Map<String, Identifier> categoryBackground = Maps.newHashMap();
+    private Map<String, List<Pair<String, Object>>> dataMap = Maps.newLinkedHashMap();
+    
+    @Deprecated
+    public ConfigBuilderImpl() {
+    
+    }
+    
+    @Override
+    public ConfigBuilder setAfterInitConsumer(Consumer<Screen> afterInitConsumer) {
+        this.afterInitConsumer = afterInitConsumer;
+        return this;
+    }
+    
+    @Override
+    public Screen getParentScreen() {
+        return parent;
+    }
+    
+    @Override
+    public ConfigBuilder setParentScreen(Screen parent) {
+        this.parent = parent;
+        return this;
+    }
+    
+    @Override
+    public String getTitle() {
+        return title;
+    }
+    
+    @Override
+    public ConfigBuilder setTitle(String title) {
+        this.title = title;
+        return this;
+    }
+    
+    @Override
+    public boolean isEditable() {
+        return editable;
+    }
+    
+    @Override
+    public ConfigBuilder setEditable(boolean editable) {
+        this.editable = editable;
+        return this;
+    }
+    
+    @Override
+    public ConfigCategory getOrCreateCategory(String categoryKey) {
+        if (dataMap.containsKey(categoryKey))
+            return new ConfigCategoryImpl(identifier -> {
+                categoryBackground.put(categoryKey, identifier);
+            }, () -> dataMap.get(categoryKey));
+        dataMap.put(categoryKey, Lists.newArrayList());
+        return new ConfigCategoryImpl(identifier -> {
+            categoryBackground.put(categoryKey, identifier);
+        }, () -> dataMap.get(categoryKey));
+    }
+    
+    @Override
+    public ConfigBuilder removeCategory(String category) {
+        dataMap.remove(category);
+        return this;
+    }
+    
+    @Override
+    public ConfigBuilder removeCategoryIfExists(String category) {
+        if (dataMap.containsKey(category))
+            dataMap.remove(category);
+        return this;
+    }
+    
+    @Override
+    public boolean hasCategory(String category) {
+        return dataMap.containsKey(category);
+    }
+    
+    @Override
+    public ConfigBuilder setShouldTabsSmoothScroll(boolean shouldTabsSmoothScroll) {
+        this.tabsSmoothScroll = shouldTabsSmoothScroll;
+        return this;
+    }
+    
+    @Override
+    public boolean isTabsSmoothScrolling() {
+        return tabsSmoothScroll;
+    }
+    
+    @Override
+    public ConfigBuilder setShouldListSmoothScroll(boolean shouldListSmoothScroll) {
+        this.listSmoothScroll = shouldListSmoothScroll;
+        return this;
+    }
+    
+    @Override
+    public boolean isListSmoothScrolling() {
+        return listSmoothScroll;
+    }
+    
+    @Override
+    public ConfigBuilder setDoesConfirmSave(boolean confirmSave) {
+        this.doesConfirmSave = confirmSave;
+        return this;
+    }
+    
+    @Override
+    public boolean doesConfirmSave() {
+        return doesConfirmSave;
+    }
+    
+    @Override
+    public ConfigBuilder setDoesProcessErrors(boolean processErrors) {
+        this.doesProcessErrors = processErrors;
+        return this;
+    }
+    
+    @Override
+    public boolean doesProcessErrors() {
+        return doesProcessErrors;
+    }
+    
+    @Override
+    public Identifier getDefaultBackgroundTexture() {
+        return defaultBackground;
+    }
+    
+    @Override
+    public ConfigBuilder setDefaultBackgroundTexture(Identifier texture) {
+        this.defaultBackground = texture;
+        return this;
+    }
+    
+    @Override
+    public ConfigBuilder setSavingRunnable(Runnable runnable) {
+        this.savingRunnable = runnable;
+        return this;
+    }
+    
+    @Override
+    public Consumer<Screen> getAfterInitConsumer() {
+        return afterInitConsumer;
+    }
+    
+    @Override
+    public Screen build() {
+        ClothConfigScreen screen = new ClothConfigScreen(parent, title, dataMap, doesConfirmSave, doesProcessErrors, listSmoothScroll, defaultBackground, categoryBackground) {
+            @Override
+            public void onSave(Map<String, List<Pair<String, Object>>> o) {
+                savingRunnable.run();
+            }
+            
+            @Override
+            public boolean isEditable() {
+                return editable;
+            }
+            
+            @Override
+            protected void init() {
+                super.init();
+                afterInitConsumer.accept(this);
+            }
+        };
+        screen.setSmoothScrollingTabs(tabsSmoothScroll);
+        return screen;
+    }
+    
+    @Override
+    public Runnable getSavingRunnable() {
+        return savingRunnable;
+    }
+    
+}

+ 40 - 0
src/main/java/me/shedaniel/clothconfig2/impl/ConfigCategoryImpl.java

@@ -0,0 +1,40 @@
+package me.shedaniel.clothconfig2.impl;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.api.ConfigCategory;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class ConfigCategoryImpl implements ConfigCategory {
+    
+    private Supplier<List<Pair<String, Object>>> listSupplier;
+    private Consumer<Identifier> backgroundConsumer;
+    
+    public ConfigCategoryImpl(Consumer<Identifier> backgroundConsumer, Supplier<List<Pair<String, Object>>> listSupplier) {
+        this.listSupplier = listSupplier;
+        this.backgroundConsumer = backgroundConsumer;
+    }
+    
+    @Override
+    public List<Object> getEntries() {
+        return listSupplier.get().stream().map(Pair::getRight).collect(Collectors.toList());
+    }
+    
+    @Override
+    public ConfigCategory addEntry(AbstractConfigListEntry entry) {
+        listSupplier.get().add(new Pair<>(entry.getFieldName(), entry));
+        return this;
+    }
+    
+    @Override
+    public ConfigCategory setCategoryBackground(Identifier identifier) {
+        backgroundConsumer.accept(identifier);
+        return this;
+    }
+    
+}

+ 94 - 0
src/main/java/me/shedaniel/clothconfig2/impl/ConfigEntryBuilderImpl.java

@@ -0,0 +1,94 @@
+package me.shedaniel.clothconfig2.impl;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
+import me.shedaniel.clothconfig2.impl.builders.*;
+
+import java.util.List;
+import java.util.UUID;
+
+public class ConfigEntryBuilderImpl implements ConfigEntryBuilder {
+    
+    private String resetButtonKey = "text.cloth-config.reset_value";
+    
+    private ConfigEntryBuilderImpl() {
+    }
+    
+    public static ConfigEntryBuilderImpl create() {
+        return new ConfigEntryBuilderImpl();
+    }
+    
+    @Override
+    public String getResetButtonKey() {
+        return resetButtonKey;
+    }
+    
+    @Override
+    public ConfigEntryBuilder setResetButtonKey(String resetButtonKey) {
+        this.resetButtonKey = resetButtonKey;
+        return this;
+    }
+    
+    @Override
+    public SubCategoryBuilder startSubCategory(String fieldNameKey) {
+        return new SubCategoryBuilder(resetButtonKey, fieldNameKey);
+    }
+    
+    @Override
+    public SubCategoryBuilder startSubCategory(String fieldNameKey, List<AbstractConfigListEntry> entries) {
+        SubCategoryBuilder builder = new SubCategoryBuilder(resetButtonKey, fieldNameKey);
+        builder.addAll(entries);
+        return builder;
+    }
+    
+    @Override
+    public BooleanToggleBuilder startBooleanToggle(String fieldNameKey, boolean value) {
+        return new BooleanToggleBuilder(resetButtonKey, fieldNameKey, value);
+    }
+    
+    @Override
+    public TextFieldBuilder startTextField(String fieldNameKey, String value) {
+        return new TextFieldBuilder(resetButtonKey, fieldNameKey, value);
+    }
+    
+    @Override
+    public TextDescriptionBuilder startTextDescription(String value) {
+        return new TextDescriptionBuilder(resetButtonKey, UUID.randomUUID().toString(), value);
+    }
+    
+    @Override
+    public <T extends Enum<?>> EnumSelectorBuilder<T> startEnumSelector(String fieldNameKey, Class<T> clazz, T value) {
+        return new EnumSelectorBuilder<T>(resetButtonKey, fieldNameKey, clazz, value);
+    }
+    
+    @Override
+    public IntFieldBuilder startIntField(String fieldNameKey, int value) {
+        return new IntFieldBuilder(resetButtonKey, fieldNameKey, value);
+    }
+    
+    @Override
+    public LongFieldBuilder startLongField(String fieldNameKey, long value) {
+        return new LongFieldBuilder(resetButtonKey, fieldNameKey, value);
+    }
+    
+    @Override
+    public FloatFieldBuilder startFloatField(String fieldNameKey, float value) {
+        return new FloatFieldBuilder(resetButtonKey, fieldNameKey, value);
+    }
+    
+    @Override
+    public DoubleFieldBuilder startDoubleField(String fieldNameKey, double value) {
+        return new DoubleFieldBuilder(resetButtonKey, fieldNameKey, value);
+    }
+    
+    @Override
+    public IntSliderBuilder startIntSlider(String fieldNameKey, int value, int min, int max) {
+        return new IntSliderBuilder(resetButtonKey, fieldNameKey, value, min, max);
+    }
+    
+    @Override
+    public LongSliderBuilder startLongSlider(String fieldNameKey, long value, long min, long max) {
+        return new LongSliderBuilder(resetButtonKey, fieldNameKey, value, min, max);
+    }
+    
+}

+ 55 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/BooleanToggleBuilder.java

@@ -0,0 +1,55 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.BooleanListEntry;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class BooleanToggleBuilder extends FieldBuilder<Boolean> {
+    
+    private Consumer<Boolean> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private boolean value;
+    private Function<Boolean, String> yesNoTextSupplier = bool -> bool ? "§aYes" : "§cNo";
+    
+    public BooleanToggleBuilder(String resetButtonKey, String fieldNameKey, boolean value) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+    }
+    
+    public BooleanToggleBuilder setSaveConsumer(Consumer<Boolean> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public BooleanToggleBuilder setDefaultValue(Supplier<Boolean> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public BooleanToggleBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public BooleanToggleBuilder setYesNoTextSupplier(Function<Boolean, String> yesNoTextSupplier) {
+        Objects.requireNonNull(yesNoTextSupplier);
+        this.yesNoTextSupplier = yesNoTextSupplier;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        return new BooleanListEntry(getFieldNameKey(), value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier) {
+            @Override
+            public String getYesNoText(boolean bool) {
+                return yesNoTextSupplier.apply(bool);
+            }
+        };
+    }
+    
+}

+ 67 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/DoubleFieldBuilder.java

@@ -0,0 +1,67 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.DoubleListEntry;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class DoubleFieldBuilder extends FieldBuilder<Double> {
+    
+    private Consumer<Double> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private double value;
+    private Double min = null, max = null;
+    
+    public DoubleFieldBuilder(String resetButtonKey, String fieldNameKey, double value) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+    }
+    
+    public DoubleFieldBuilder setSaveConsumer(Consumer<Double> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public DoubleFieldBuilder setDefaultValue(Supplier<Double> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public DoubleFieldBuilder setMin(double min) {
+        this.min = min;
+        return this;
+    }
+    
+    public DoubleFieldBuilder setMax(double max) {
+        this.max = max;
+        return this;
+    }
+    
+    public DoubleFieldBuilder removeMin() {
+        this.min = null;
+        return this;
+    }
+    
+    public DoubleFieldBuilder removeMax() {
+        this.max = null;
+        return this;
+    }
+    
+    public DoubleFieldBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        DoubleListEntry entry = new DoubleListEntry(getFieldNameKey(), value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier);
+        if (min != null)
+            entry.setMinimum(min);
+        if (max != null)
+            entry.setMaximum(max);
+        return entry;
+    }
+    
+}

+ 54 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/EnumSelectorBuilder.java

@@ -0,0 +1,54 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.EnumListEntry;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class EnumSelectorBuilder<T extends Enum<?>> extends FieldBuilder<T> {
+    
+    private Consumer<T> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private T value;
+    private Class<T> clazz;
+    private Function<Enum, String> enumNameProvider = EnumListEntry.DEFAULT_NAME_PROVIDER;
+    
+    public EnumSelectorBuilder(String resetButtonKey, String fieldNameKey, Class<T> clazz, T value) {
+        super(resetButtonKey, fieldNameKey);
+        Objects.requireNonNull(clazz);
+        Objects.requireNonNull(value);
+        this.value = value;
+        this.clazz = clazz;
+    }
+    
+    public EnumSelectorBuilder setSaveConsumer(Consumer<T> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public EnumSelectorBuilder setDefaultValue(Supplier<T> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public EnumSelectorBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public EnumSelectorBuilder setEnumNameProvider(Function<Enum, String> enumNameProvider) {
+        Objects.requireNonNull(enumNameProvider);
+        this.enumNameProvider = enumNameProvider;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        return new EnumListEntry(getFieldNameKey(), clazz, value, getResetButtonKey(), defaultValue, saveConsumer, enumNameProvider, tooltipSupplier);
+    }
+    
+}

+ 31 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/FieldBuilder.java

@@ -0,0 +1,31 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+
+import java.util.function.Supplier;
+
+public abstract class FieldBuilder<T> {
+    private final String fieldNameKey;
+    private final String resetButtonKey;
+    protected Supplier<T> defaultValue = null;
+    
+    protected FieldBuilder(String resetButtonKey, String fieldNameKey) {
+        this.resetButtonKey = resetButtonKey;
+        this.fieldNameKey = fieldNameKey;
+    }
+    
+    public final Supplier<T> getDefaultValue() {
+        return defaultValue;
+    }
+    
+    public abstract AbstractConfigListEntry buildEntry();
+    
+    public final String getFieldNameKey() {
+        return fieldNameKey;
+    }
+    
+    public final String getResetButtonKey() {
+        return resetButtonKey;
+    }
+    
+}

+ 67 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/FloatFieldBuilder.java

@@ -0,0 +1,67 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.FloatListEntry;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class FloatFieldBuilder extends FieldBuilder<Float> {
+    
+    private Consumer<Float> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private float value;
+    private Float min = null, max = null;
+    
+    public FloatFieldBuilder(String resetButtonKey, String fieldNameKey, float value) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+    }
+    
+    public FloatFieldBuilder setSaveConsumer(Consumer<Float> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public FloatFieldBuilder setDefaultValue(Supplier<Float> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public FloatFieldBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public FloatFieldBuilder setMin(float min) {
+        this.min = min;
+        return this;
+    }
+    
+    public FloatFieldBuilder setMax(float max) {
+        this.max = max;
+        return this;
+    }
+    
+    public FloatFieldBuilder removeMin() {
+        this.min = null;
+        return this;
+    }
+    
+    public FloatFieldBuilder removeMax() {
+        this.max = null;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        FloatListEntry entry = new FloatListEntry(getFieldNameKey(), value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier);
+        if (min != null)
+            entry.setMinimum(min);
+        if (max != null)
+            entry.setMaximum(max);
+        return entry;
+    }
+    
+}

+ 67 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/IntFieldBuilder.java

@@ -0,0 +1,67 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.IntegerListEntry;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class IntFieldBuilder extends FieldBuilder<Integer> {
+    
+    private Consumer<Integer> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private int value;
+    private Integer min = null, max = null;
+    
+    public IntFieldBuilder(String resetButtonKey, String fieldNameKey, int value) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+    }
+    
+    public IntFieldBuilder setSaveConsumer(Consumer<Integer> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public IntFieldBuilder setDefaultValue(Supplier<Integer> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public IntFieldBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public IntFieldBuilder setMin(int min) {
+        this.min = min;
+        return this;
+    }
+    
+    public IntFieldBuilder setMax(int max) {
+        this.max = max;
+        return this;
+    }
+    
+    public IntFieldBuilder removeMin() {
+        this.min = null;
+        return this;
+    }
+    
+    public IntFieldBuilder removeMax() {
+        this.max = null;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        IntegerListEntry entry = new IntegerListEntry(getFieldNameKey(), value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier);
+        if (min != null)
+            entry.setMinimum(min);
+        if (max != null)
+            entry.setMaximum(max);
+        return entry;
+    }
+    
+}

+ 62 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/IntSliderBuilder.java

@@ -0,0 +1,62 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.IntegerSliderEntry;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class IntSliderBuilder extends FieldBuilder<Integer> {
+    
+    private Consumer<Integer> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private int value, max, min;
+    private Function<Integer, String> textGetter = null;
+    
+    public IntSliderBuilder(String resetButtonKey, String fieldNameKey, int value, int min, int max) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+        this.max = max;
+        this.min = min;
+    }
+    
+    public IntSliderBuilder setTextGetter(Function<Integer, String> textGetter) {
+        this.textGetter = textGetter;
+        return this;
+    }
+    
+    public IntSliderBuilder setSaveConsumer(Consumer<Integer> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public IntSliderBuilder setDefaultValue(Supplier<Integer> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public IntSliderBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public IntSliderBuilder setMax(int max) {
+        this.max = max;
+        return this;
+    }
+    
+    public IntSliderBuilder setMin(int min) {
+        this.min = min;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        if (textGetter == null)
+            return new IntegerSliderEntry(getFieldNameKey(), min, max, value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier);
+        return new IntegerSliderEntry(getFieldNameKey(), min, max, value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier).setTextGetter(textGetter);
+    }
+    
+}

+ 67 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/LongFieldBuilder.java

@@ -0,0 +1,67 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.LongListEntry;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class LongFieldBuilder extends FieldBuilder<Long> {
+    
+    private Consumer<Long> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private long value;
+    private Long min = null, max = null;
+    
+    public LongFieldBuilder(String resetButtonKey, String fieldNameKey, long value) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+    }
+    
+    public LongFieldBuilder setSaveConsumer(Consumer<Long> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public LongFieldBuilder setDefaultValue(Supplier<Long> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public LongFieldBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public LongFieldBuilder setMin(long min) {
+        this.min = min;
+        return this;
+    }
+    
+    public LongFieldBuilder setMax(long max) {
+        this.max = max;
+        return this;
+    }
+    
+    public LongFieldBuilder removeMin() {
+        this.min = null;
+        return this;
+    }
+    
+    public LongFieldBuilder removeMax() {
+        this.max = null;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        LongListEntry entry = new LongListEntry(getFieldNameKey(), value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier);
+        if (min != null)
+            entry.setMinimum(min);
+        if (max != null)
+            entry.setMaximum(max);
+        return entry;
+    }
+    
+}

+ 52 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/LongSliderBuilder.java

@@ -0,0 +1,52 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.LongSliderEntry;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class LongSliderBuilder extends FieldBuilder<Long> {
+    
+    private Consumer<Long> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private long value, max, min;
+    private Function<Long, String> textGetter = null;
+    
+    public LongSliderBuilder(String resetButtonKey, String fieldNameKey, long value, long min, long max) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+        this.max = max;
+        this.min = min;
+    }
+    
+    public LongSliderBuilder setTextGetter(Function<Long, String> textGetter) {
+        this.textGetter = textGetter;
+        return this;
+    }
+    
+    public LongSliderBuilder setSaveConsumer(Consumer<Long> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public LongSliderBuilder setDefaultValue(Supplier<Long> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public LongSliderBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        if (textGetter == null)
+            return new LongSliderEntry(getFieldNameKey(), min, max, value, saveConsumer, getResetButtonKey(), defaultValue, tooltipSupplier);
+        return new LongSliderEntry(getFieldNameKey(), min, max, value, saveConsumer, getResetButtonKey(), defaultValue, tooltipSupplier).setTextGetter(textGetter);
+    }
+    
+}

+ 153 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/SubCategoryBuilder.java

@@ -0,0 +1,153 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.SubCategoryListEntry;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+public class SubCategoryBuilder extends FieldBuilder implements List<AbstractConfigListEntry> {
+    
+    private List<AbstractConfigListEntry> entries;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private boolean expended = false;
+    
+    public SubCategoryBuilder(String resetButtonKey, String fieldNameKey) {
+        super(resetButtonKey, fieldNameKey);
+        this.entries = Lists.newArrayList();
+    }
+    
+    public SubCategoryBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public SubCategoryBuilder setExpended(boolean expended) {
+        this.expended = expended;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        SubCategoryListEntry entry = new SubCategoryListEntry(getFieldNameKey(), entries, expended);
+        entry.setTooltipSupplier(tooltipSupplier);
+        return entry;
+    }
+    
+    @Override
+    public int size() {
+        return entries.size();
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return entries.isEmpty();
+    }
+    
+    @Override
+    public boolean contains(Object o) {
+        return entries.contains(o);
+    }
+    
+    @Override
+    public Iterator<AbstractConfigListEntry> iterator() {
+        return entries.iterator();
+    }
+    
+    @Override
+    public Object[] toArray() {
+        return entries.toArray();
+    }
+    
+    @Override
+    public <T> T[] toArray(T[] a) {
+        return entries.toArray(a);
+    }
+    
+    @Override
+    public boolean add(AbstractConfigListEntry abstractConfigListEntry) {
+        return entries.add(abstractConfigListEntry);
+    }
+    
+    @Override
+    public boolean remove(Object o) {
+        return entries.remove(o);
+    }
+    
+    @Override
+    public boolean containsAll(Collection<?> c) {
+        return entries.containsAll(c);
+    }
+    
+    @Override
+    public boolean addAll(Collection<? extends AbstractConfigListEntry> c) {
+        return entries.addAll(c);
+    }
+    
+    @Override
+    public boolean addAll(int index, Collection<? extends AbstractConfigListEntry> c) {
+        return entries.addAll(index, c);
+    }
+    
+    @Override
+    public boolean removeAll(Collection<?> c) {
+        return entries.removeAll(c);
+    }
+    
+    @Override
+    public boolean retainAll(Collection<?> c) {
+        return entries.retainAll(c);
+    }
+    
+    @Override
+    public void clear() {
+        entries.clear();
+    }
+    
+    @Override
+    public AbstractConfigListEntry get(int index) {
+        return entries.get(index);
+    }
+    
+    @Override
+    public AbstractConfigListEntry set(int index, AbstractConfigListEntry element) {
+        return entries.set(index, element);
+    }
+    
+    @Override
+    public void add(int index, AbstractConfigListEntry element) {
+        entries.add(index, element);
+    }
+    
+    @Override
+    public AbstractConfigListEntry remove(int index) {
+        return entries.remove(index);
+    }
+    
+    @Override
+    public int indexOf(Object o) {
+        return entries.indexOf(o);
+    }
+    
+    @Override
+    public int lastIndexOf(Object o) {
+        return entries.lastIndexOf(o);
+    }
+    
+    @Override
+    public ListIterator<AbstractConfigListEntry> listIterator() {
+        return entries.listIterator();
+    }
+    
+    @Override
+    public ListIterator<AbstractConfigListEntry> listIterator(int index) {
+        return entries.listIterator(index);
+    }
+    
+    @Override
+    public List<AbstractConfigListEntry> subList(int fromIndex, int toIndex) {
+        return entries.subList(fromIndex, toIndex);
+    }
+    
+}

+ 35 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/TextDescriptionBuilder.java

@@ -0,0 +1,35 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.TextListEntry;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+public class TextDescriptionBuilder extends FieldBuilder {
+    
+    private int color = -1;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private String value;
+    
+    public TextDescriptionBuilder(String resetButtonKey, String fieldNameKey, String value) {
+        super(resetButtonKey, fieldNameKey);
+        this.value = value;
+    }
+    
+    public TextDescriptionBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public TextDescriptionBuilder setColor(int color) {
+        this.color = color;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        return new TextListEntry(getFieldNameKey(), value, color, tooltipSupplier);
+    }
+    
+}

+ 43 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/TextFieldBuilder.java

@@ -0,0 +1,43 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.entries.StringListEntry;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class TextFieldBuilder extends FieldBuilder<String> {
+    
+    private Consumer<String> saveConsumer = null;
+    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    private String value;
+    
+    public TextFieldBuilder(String resetButtonKey, String fieldNameKey, String value) {
+        super(resetButtonKey, fieldNameKey);
+        Objects.requireNonNull(value);
+        this.value = value;
+    }
+    
+    public TextFieldBuilder setSaveConsumer(Consumer<String> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public TextFieldBuilder setDefaultValue(Supplier<String> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public TextFieldBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    @Override
+    public AbstractConfigListEntry buildEntry() {
+        return new StringListEntry(getFieldNameKey(), value, getResetButtonKey(), defaultValue, saveConsumer, tooltipSupplier);
+    }
+    
+}

+ 19 - 0
src/main/resources/assets/cloth-config2/lang/en_us.json

@@ -0,0 +1,19 @@
+{
+  "text.cloth-config.save_and_done": "Save Changes",
+  "text.cloth-config.quit_config": "Changes Not Saved",
+  "text.cloth-config.quit_config_sure": "Are you sure you want to quit editing the config? Changes will not be saved!",
+  "text.cloth-config.cancel_discard": "Cancel & Discard Changes",
+  "text.cloth-config.quit_discard": "Quit & Discard Changes",
+  "text.cloth-config.config": "Config",
+  "text.cloth-config.multi_error": "Multiple Issues!",
+  "text.cloth-config.not_editable": "Not Editable!",
+  "text.cloth-config.error.not_valid_number_int": "Not a valid number! (Integer)",
+  "text.cloth-config.error.not_valid_number_long": "Not a valid number! (Long)",
+  "text.cloth-config.error.not_valid_number_float": "Not a valid number! (Float)",
+  "text.cloth-config.error.not_valid_number_double": "Not a valid number! (Double)",
+  "text.cloth-config.error.too_large": "Too Large! (Maximum: %d)",
+  "text.cloth-config.error.too_small": "Too Small! (Minimum: %d)",
+  "text.cloth-config.error_cannot_save": "Error!",
+  "text.cloth-config.reset_value": "Reset",
+  "text.cloth.reset_value": "Reset"
+}

+ 18 - 0
src/main/resources/assets/cloth-config2/lang/fr_ca.json

@@ -0,0 +1,18 @@
+{
+  "text.cloth-config.save_and_done": "Sauvegarder",
+  "text.cloth-config.quit_config": "Modifications non sauvegardées",
+  "text.cloth-config.quit_config_sure": "Voulez-vous quitter l'écran de configuration? Les modifications ne seront pas sauvegardées!",
+  "text.cloth-config.cancel_discard": "Annuler les modifications",
+  "text.cloth-config.quit_discard": "Quitter quand même",
+  "text.cloth-config.config": "Configuration",
+  "text.cloth-config.multi_error": "Problèmes multiples!",
+  "text.cloth-config.error.not_valid_number_int": "Pas un entier valide! (Integer)",
+  "text.cloth-config.error.not_valid_number_long": "Pas un nombre invalide! (Long)",
+  "text.cloth-config.error.not_valid_number_float": "Pas un nombre invalide! (Float)",
+  "text.cloth-config.error.not_valid_number_double": "Pas un nombre invalide! (Double)",
+  "text.cloth-config.error.too_large": "Trop grand! (Maximum: %d)",
+  "text.cloth-config.error.too_small": "Trop petit! (Minimum: %d)",
+  "text.cloth-config.error_cannot_save": "Erreur!",
+  "text.cloth-config.reset_value": "Réinit.",
+  "text.cloth.reset_value": "Réinit."
+}

+ 18 - 0
src/main/resources/assets/cloth-config2/lang/fr_fr.json

@@ -0,0 +1,18 @@
+{
+  "text.cloth-config.save_and_done": "Sauvegarder",
+  "text.cloth-config.quit_config": "Modifications non sauvegardées",
+  "text.cloth-config.quit_config_sure": "Voulez-vous quitter l'écran de configuration? Les modifications ne seront pas sauvegardées!",
+  "text.cloth-config.cancel_discard": "Annuler les modifications",
+  "text.cloth-config.quit_discard": "Quitter quand même",
+  "text.cloth-config.config": "Configuration",
+  "text.cloth-config.multi_error": "Problèmes multiples!",
+  "text.cloth-config.error.not_valid_number_int": "Pas un entier valide! (Integer)",
+  "text.cloth-config.error.not_valid_number_long": "Pas un nombre invalide! (Long)",
+  "text.cloth-config.error.not_valid_number_float": "Pas un nombre invalide! (Float)",
+  "text.cloth-config.error.not_valid_number_double": "Pas un nombre invalide! (Double)",
+  "text.cloth-config.error.too_large": "Trop grand! (Maximum: %d)",
+  "text.cloth-config.error.too_small": "Trop petit! (Minimum: %d)",
+  "text.cloth-config.error_cannot_save": "Erreur!",
+  "text.cloth-config.reset_value": "Réinit.",
+  "text.cloth.reset_value": "Réinit."
+}

BIN
src/main/resources/assets/cloth-config2/textures/gui/cloth_config.png


+ 30 - 0
src/main/resources/fabric.mod.json

@@ -0,0 +1,30 @@
+{
+  "schemaVersion": 1,
+  "id": "cloth-config2",
+  "name": "Cloth Config v2",
+  "description": "An API for config screens.",
+  "version": "${version}",
+  "authors": [
+    "Danielshe"
+  ],
+  "contact": {
+    "homepage": "https://minecraft.curseforge.com/projects/cloth-config",
+    "sources": "https://github.com/shedaniel/ClothConfig",
+    "issues": "https://github.com/shedaniel/ClothConfig/issues"
+  },
+  "license": "Unlicense",
+  "icon": "icon.png",
+  "environment": "client",
+  "entrypoints": {
+    "client": [
+      "me.shedaniel.clothconfig2.ClothConfigInitializer"
+    ]
+  },
+  "requires": {
+    "fabricloader": ">=0.4.0"
+  },
+  "custom": {
+    "modmenu:api": true,
+    "modmenu:clientsideOnly": true
+  }
+}

BIN
src/main/resources/icon.png