فهرست منبع

2.1 Dropdown Menu

Danielshe 5 سال پیش
والد
کامیت
4bf878d75e
20فایلهای تغییر یافته به همراه1521 افزوده شده و 76 حذف شده
  1. 32 36
      README.md
  2. 1 0
      build.gradle
  3. 1 1
      gradle.properties
  4. 14 1
      src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java
  5. 2 0
      src/main/java/me/shedaniel/clothconfig2/api/AbstractConfigEntry.java
  6. 43 0
      src/main/java/me/shedaniel/clothconfig2/api/ConfigEntryBuilder.java
  7. 39 4
      src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java
  8. 1 1
      src/main/java/me/shedaniel/clothconfig2/gui/entries/BooleanListEntry.java
  9. 760 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/DropdownBoxEntry.java
  10. 2 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/SelectionListEntry.java
  11. 8 4
      src/main/java/me/shedaniel/clothconfig2/gui/entries/TooltipListEntry.java
  12. 41 10
      src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicEntryListWidget.java
  13. 6 1
      src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicNewSmoothScrollingEntryListWidget.java
  14. 5 0
      src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicSmoothScrollingEntryListWidget.java
  15. 8 0
      src/main/java/me/shedaniel/clothconfig2/impl/ConfigEntryBuilderImpl.java
  16. 17 10
      src/main/java/me/shedaniel/clothconfig2/impl/builders/BooleanToggleBuilder.java
  17. 521 0
      src/main/java/me/shedaniel/clothconfig2/impl/builders/DropdownMenuBuilder.java
  18. 14 6
      src/main/java/me/shedaniel/clothconfig2/impl/builders/FieldBuilder.java
  19. 2 1
      src/main/java/me/shedaniel/clothconfig2/impl/builders/TextDescriptionBuilder.java
  20. 4 1
      src/main/resources/assets/cloth-config2/lang/en_us.json

+ 32 - 36
README.md

@@ -9,42 +9,6 @@ dependencies {
 }
 ```
 ## 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 nameProvider 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
@@ -65,3 +29,35 @@ Lastly, you can open the screen like this:
 ```java
 MinecraftClient.getInstance().openScreen(builder.build());
 ```
+
+#### Dropdown Menus
+Start by doing `entryBuilder.startDropdownMenu()`, the `SelectionTopCellElement` is the search bar, and `SelectionCellCreator` is the cells below.
+
+Create a `SelectionTopCellElement` with `DropdownMenuBuilder.TopCellElementBuilder.of()`, which takes three parameters:
+- `value`: The value of the field
+- `toObjectFunction`: The toObject function, turning String into T, returns null if error
+- `toStringFunction`: The toString function, which never returns null, affects the displayed text of your value.
+
+You can also use the premade `SelectionTopCellElement` for items and blocks.
+
+
+Create a `SelectionCellCreator` with `DropdownMenuBuilder.CellCreatorBuilder.of()`, which defines the cell height, the cell width, and how many cells are displayed at most.
+- `toStringFunction`: The toString function, which never returns null, affects the displayed text of the cell.
+
+You can also use the premade `SelectionCellCreator` for items and blocks as well.
+
+You should create your own cell creator extending the `DefaultSelectionCellCreator` to create custom cells.
+
+Do `.setSelections()` with your builder to specify the list of suggestions.
+
+This is what you should do if you got a config for items:
+```java
+entryBuilder.startDropdownMenu("Field Key", 
+    DropdownMenuBuilder.TopCellElementBuilder.ofItemObject(configItem), // This should contain your saved item instead of an apple as shown here 
+    DropdownMenuBuilder.CellCreatorBuilder.ofItemObject()
+)
+    .setDefaultValue(Items.APPLE) // You should define a default value here
+    .setSelections(Registry.ITEM.stream().collect(Collectors.toSet()))
+    .setSaveConsumer(item -> configItem = (Item) item) // You should save it here, cast the item because Java is "smart"
+    .build();
+```

+ 1 - 0
build.gradle

@@ -38,6 +38,7 @@ dependencies {
     modApi "net.fabricmc:fabric-loader:${project.fabric_loader_version}"
     modApi "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
     modImplementation "io.github.prospector:modmenu:${modmenu_version}"
+    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
 }
 
 bintray {

+ 1 - 1
gradle.properties

@@ -2,5 +2,5 @@ minecraft_version=19w42a
 yarn_version=19w42a+build.1
 fabric_loader_version=0.6.3+build.167
 fabric_version=0.4.6+build.251-1.15
-mod_version=2.0.2-unstable
+mod_version=2.1.0-unstable
 modmenu_version=1.7.14-unstable.19w42a+build.10

+ 14 - 1
src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java

@@ -6,10 +6,14 @@ import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
 import me.shedaniel.clothconfig2.impl.EasingMethod;
 import me.shedaniel.clothconfig2.impl.EasingMethod.EasingMethodImpl;
 import me.shedaniel.clothconfig2.impl.EasingMethods;
+import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder;
 import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.client.MinecraftClient;
+import net.minecraft.item.Item;
+import net.minecraft.item.Items;
 import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.Registry;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -17,7 +21,9 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileWriter;
 import java.lang.reflect.Method;
+import java.util.Comparator;
 import java.util.Properties;
+import java.util.stream.Collectors;
 
 public class ClothConfigInitializer implements ClientModInitializer {
     
@@ -114,12 +120,19 @@ public class ClothConfigInitializer implements ClientModInitializer {
                         builder.setDefaultBackgroundTexture(new Identifier("minecraft:textures/block/oak_planks.png"));
                         ConfigCategory scrolling = builder.getOrCreateCategory("Scrolling");
                         ConfigEntryBuilder entryBuilder = ConfigEntryBuilder.create();
-                        scrolling.addEntry(entryBuilder.startSelector("Easing Method", EasingMethods.getMethods().toArray(new EasingMethod[0]), easingMethod).setDefaultValue(EasingMethodImpl.QUART).setSaveConsumer(o -> easingMethod = (EasingMethod) o).build());
+                        scrolling.addEntry(entryBuilder.startDropdownMenu("Easing Method", DropdownMenuBuilder.TopCellElementBuilder.of(easingMethod, str -> {
+                            for(EasingMethod m : EasingMethods.getMethods())
+                                if (m.toString().equals(str))
+                                    return m;
+                            return null;
+                        })).setDefaultValue(EasingMethodImpl.QUART).setSaveConsumer(o -> easingMethod = (EasingMethod) o).setSelections(EasingMethods.getMethods()).build());
                         scrolling.addEntry(entryBuilder.startLongSlider("Scroll Duration", scrollDuration, 0, 5000).setTextGetter(integer -> {
                             return integer <= 0 ? "Value: Disabled" : (integer > 1500 ? String.format("Value: %.1fs", integer / 1000f) : "Value: " + integer + "ms");
                         }).setDefaultValue(1000).setSaveConsumer(i -> scrollDuration = i).build());
                         scrolling.addEntry(entryBuilder.startDoubleField("Scroll Step", scrollStep).setDefaultValue(16).setSaveConsumer(i -> scrollStep = i).build());
                         scrolling.addEntry(entryBuilder.startDoubleField("Bounce Multiplier", bounceBackMultiplier).setDefaultValue(0.84).setSaveConsumer(i -> bounceBackMultiplier = i).build());
+                        ConfigCategory testing = builder.getOrCreateCategory("Testing");
+                        testing.addEntry(entryBuilder.startDropdownMenu("lol apple", DropdownMenuBuilder.TopCellElementBuilder.ofItemObject(Items.APPLE), DropdownMenuBuilder.CellCreatorBuilder.ofItemObject()).setDefaultValue(Items.APPLE).setSelections(Registry.ITEM.stream().sorted(Comparator.comparing(Item::toString)).collect(Collectors.toSet())).setSaveConsumer(item -> System.out.println("save this " + item)).build());
                         builder.setSavingRunnable(() -> {
                             saveConfig();
                         });

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

@@ -24,6 +24,8 @@ public abstract class AbstractConfigEntry<T> extends DynamicElementListWidget.El
         return getError();
     }
     
+    public void lateRender(int mouseX, int mouseY, float delta) {}
+    
     public void setErrorSupplier(Supplier<Optional<String>> errorSupplier) {
         this.errorSupplier = errorSupplier;
     }

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

@@ -1,9 +1,14 @@
 package me.shedaniel.clothconfig2.api;
 
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.DefaultSelectionCellCreator;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionCellCreator;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionTopCellElement;
 import me.shedaniel.clothconfig2.impl.ConfigEntryBuilderImpl;
 import me.shedaniel.clothconfig2.impl.builders.*;
+import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder.TopCellElementBuilder;
 
 import java.util.List;
+import java.util.function.Function;
 
 public interface ConfigEntryBuilder {
     
@@ -52,4 +57,42 @@ public interface ConfigEntryBuilder {
     IntSliderBuilder startIntSlider(String fieldNameKey, int value, int min, int max);
     
     LongSliderBuilder startLongSlider(String fieldNameKey, long value, long min, long max);
+    
+    <T> DropdownMenuBuilder<T> startDropdownMenu(String fieldNameKey, SelectionTopCellElement<T> topCellElement, SelectionCellCreator<T> cellCreator);
+    
+    default <T> DropdownMenuBuilder<T> startDropdownMenu(String fieldNameKey, SelectionTopCellElement<T> topCellElement) {
+        return startDropdownMenu(fieldNameKey, topCellElement, new DefaultSelectionCellCreator<>());
+    }
+    
+    default <T> DropdownMenuBuilder<T> startDropdownMenu(String fieldNameKey, T value, Function<String, T> toObjectFunction, SelectionCellCreator<T> cellCreator) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, toObjectFunction, Object::toString), cellCreator);
+    }
+    
+    default <T> DropdownMenuBuilder<T> startDropdownMenu(String fieldNameKey, T value, Function<String, T> toObjectFunction, Function<T, String> toStringFunction, SelectionCellCreator<T> cellCreator) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, toObjectFunction, toStringFunction), cellCreator);
+    }
+    
+    default <T> DropdownMenuBuilder<T> startDropdownMenu(String fieldNameKey, T value, Function<String, T> toObjectFunction) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, toObjectFunction, Object::toString), new DefaultSelectionCellCreator<>());
+    }
+    
+    default <T> DropdownMenuBuilder<T> startDropdownMenu(String fieldNameKey, T value, Function<String, T> toObjectFunction, Function<T, String> toStringFunction) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, toObjectFunction, toStringFunction), new DefaultSelectionCellCreator<>());
+    }
+    
+    default DropdownMenuBuilder<String> startStringDropdownMenu(String fieldNameKey, String value, SelectionCellCreator<String> cellCreator) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, s -> s, s -> s), cellCreator);
+    }
+    
+    default DropdownMenuBuilder<String> startStringDropdownMenu(String fieldNameKey, String value, Function<String, String> toStringFunction, SelectionCellCreator<String> cellCreator) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, s -> s, toStringFunction), cellCreator);
+    }
+    
+    default DropdownMenuBuilder<String> startStringDropdownMenu(String fieldNameKey, String value) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, s -> s, s -> s), new DefaultSelectionCellCreator<>());
+    }
+    
+    default DropdownMenuBuilder<String> startStringDropdownMenu(String fieldNameKey, String value, Function<String, String> toStringFunction) {
+        return startDropdownMenu(fieldNameKey, TopCellElementBuilder.of(value, s -> s, toStringFunction), new DefaultSelectionCellCreator<>());
+    }
 }

+ 39 - 4
src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java

@@ -23,12 +23,14 @@ 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.client.util.Window;
 import net.minecraft.text.LiteralText;
 import net.minecraft.text.TranslatableText;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.Pair;
 import net.minecraft.util.Tickable;
 import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
 
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -44,7 +46,7 @@ public abstract class ClothConfigScreen extends Screen {
     public int selectedTabIndex;
     public double tabsScrollVelocity = 0d;
     public double tabsScrollProgress = 0d;
-    public ListWidget listWidget;
+    public ListWidget<AbstractConfigEntry<AbstractConfigEntry>> listWidget;
     private Screen parent;
     private LinkedHashMap<String, List<AbstractConfigEntry>> tabbedEntries;
     private List<Pair<String, Integer>> tabs;
@@ -176,7 +178,7 @@ public abstract class ClothConfigScreen extends Screen {
         this.children.clear();
         this.tabButtons.clear();
         if (listWidget != null)
-            tabbedEntries.put(tabs.get(selectedTabIndex).getLeft(), listWidget.children());
+            tabbedEntries.put(tabs.get(selectedTabIndex).getLeft(), (List) listWidget.children());
         selectedTabIndex = nextTabIndex;
         children.add(listWidget = new ListWidget(minecraft, width, height, 70, height - 32, getBackgroundLocation()));
         listWidget.setSmoothScrolling(this.smoothScrollingList);
@@ -339,8 +341,19 @@ public abstract class ClothConfigScreen extends Screen {
         buttonRightTab.active = tabsScrollProgress < getTabsMaximumScrolled() - width + 40;
         renderDirtBackground(0);
         listWidget.render(int_1, int_2, float_1);
+        Window window = minecraft.getWindow();
+        int sw = window.getWidth();
+        int sh = window.getHeight();
+        int x = Math.round(sw * (listWidget.left / (float) width));
+        int y = Math.round(sh * (listWidget.top / (float) height));
+        int ww = Math.round(sw * (listWidget.width / (float) width));
+        int hh = Math.round(sh * ((listWidget.bottom - listWidget.top) / (float) height));
+        GL11.glEnable(GL11.GL_SCISSOR_TEST);
+        GL11.glScissor(x, sh - hh - y, ww, hh);
+        for(AbstractConfigEntry child : listWidget.children())
+            child.lateRender(int_1, int_2, float_1);
+        GL11.glDisable(GL11.GL_SCISSOR_TEST);
         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);
@@ -450,7 +463,7 @@ public abstract class ClothConfigScreen extends Screen {
         }
     }
     
-    public class ListWidget extends DynamicElementListWidget {
+    public class ListWidget<R extends DynamicElementListWidget.ElementEntry<R>> extends DynamicElementListWidget<R> {
         
         public ListWidget(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
             super(client, width, height, top, bottom, backgroundLocation);
@@ -474,6 +487,28 @@ public abstract class ClothConfigScreen extends Screen {
         protected final void clearStuff() {
             this.clearItems();
         }
+    
+        @Override
+        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 {
+                for(R entry : children()) {
+                    if (entry.mouseClicked(double_1, double_2, int_1)) {
+                        this.setFocused(entry);
+                        this.setDragging(true);
+                        return true;
+                    }
+                }
+                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;
+            }
+        }
     }
     
 }

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

@@ -94,7 +94,7 @@ public class BooleanListEntry extends TooltipListEntry<Boolean> {
     }
     
     public String getYesNoText(boolean bool) {
-        return bool ? "§aYes" : "§cNo";
+        return I18n.translate("text.cloth-config.boolean.value." + bool);
     }
     
     @Override

+ 760 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/DropdownBoxEntry.java

@@ -0,0 +1,760 @@
+package me.shedaniel.clothconfig2.gui.entries;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import me.shedaniel.clothconfig2.api.RunSixtyTimesEverySec;
+import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget.SmoothScrollingSettings;
+import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget.Interpolation;
+import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget.Precision;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.impl.PointHelper;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.AbstractParentElement;
+import net.minecraft.client.gui.Drawable;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+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.client.util.Window;
+import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static me.shedaniel.clothconfig2.ClothConfigInitializer.getBounceBackMultiplier;
+
+@SuppressWarnings("deprecation")
+public class DropdownBoxEntry<T> extends TooltipListEntry<T> {
+    
+    protected ButtonWidget resetButton;
+    protected SelectionElement<T> selectionElement;
+    @Nonnull private Supplier<T> defaultValue;
+    @Nullable private Consumer<T> saveConsumer;
+    
+    @Deprecated
+    public DropdownBoxEntry(String fieldName,
+            @Nonnull String resetButtonKey,
+            @Nullable Supplier<Optional<String[]>> tooltipSupplier, boolean requiresRestart,
+            @Nullable Supplier<T> defaultValue,
+            @Nullable Consumer<T> saveConsumer,
+            @Nullable Iterable<T> selections,
+            @Nonnull SelectionTopCellElement<T> topRenderer, @Nonnull SelectionCellCreator<T> cellCreator) {
+        super(I18n.translate(fieldName), tooltipSupplier, requiresRestart);
+        this.defaultValue = defaultValue;
+        this.saveConsumer = saveConsumer;
+        this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getStringWidth(I18n.translate(resetButtonKey)) + 6, 20, I18n.translate(resetButtonKey), widget -> {
+            selectionElement.topRenderer.setValue(defaultValue.get());
+            getScreen().setEdited(true, isRequiresRestart());
+        });
+        this.selectionElement = new SelectionElement(this, new Rectangle(0, 0, 150, 20), new DefaultDropdownMenuElement(selections == null ? ImmutableList.of() : ImmutableList.copyOf(selections)), topRenderer, cellCreator);
+    }
+    
+    @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().getWindow();
+        this.selectionElement.topRenderer.error = getConfigError();
+        this.resetButton.active = isEditable() && getDefaultValue().isPresent() && (!defaultValue.get().equals(getValue()) || getConfigError().isPresent());
+        this.resetButton.y = y;
+        this.selectionElement.active = isEditable();
+        this.selectionElement.bounds.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, getPreferredTextColor());
+            this.resetButton.x = x;
+            this.selectionElement.bounds.x = x + resetButton.getWidth() + 1;
+        } else {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate(getFieldName()), x, y + 5, getPreferredTextColor());
+            this.resetButton.x = x + entryWidth - resetButton.getWidth();
+            this.selectionElement.bounds.x = x + entryWidth - 150 + 1;
+        }
+        this.selectionElement.bounds.width = 150 - resetButton.getWidth() - 4;
+        resetButton.render(mouseX, mouseY, delta);
+        selectionElement.render(mouseX, mouseY, delta);
+    }
+    
+    @Nonnull
+    public ImmutableList<T> getSelections() {
+        return selectionElement.menu.getSelections();
+    }
+    
+    @Override
+    public T getValue() {
+        return selectionElement.getValue();
+    }
+    
+    @Override
+    public Optional<T> getDefaultValue() {
+        return defaultValue == null ? Optional.empty() : Optional.ofNullable(defaultValue.get());
+    }
+    
+    @Override
+    public void save() {
+        if (saveConsumer != null)
+            saveConsumer.accept(getValue());
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Lists.newArrayList(selectionElement, resetButton);
+    }
+    
+    @Override
+    public Optional<String> getError() {
+        return selectionElement.topRenderer.getError();
+    }
+    
+    @Override
+    public void lateRender(int mouseX, int mouseY, float delta) {
+        selectionElement.lateRender(mouseX, mouseY, delta);
+    }
+    
+    @Override
+    public int getMorePossibleHeight() {
+        return selectionElement.getMorePossibleHeight();
+    }
+    
+    @Override
+    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        return selectionElement.mouseScrolled(double_1, double_2, double_3);
+    }
+    
+    public static class SelectionElement<R> extends AbstractParentElement implements Drawable {
+        protected Rectangle bounds;
+        protected boolean active;
+        protected SelectionTopCellElement<R> topRenderer;
+        protected DropdownBoxEntry<R> entry;
+        protected DropdownMenuElement<R> menu;
+        protected boolean dontReFocus = false;
+        
+        public SelectionElement(DropdownBoxEntry<R> entry, Rectangle bounds, DropdownMenuElement<R> menu, SelectionTopCellElement<R> topRenderer, SelectionCellCreator<R> cellCreator) {
+            this.bounds = bounds;
+            this.entry = entry;
+            this.menu = Objects.requireNonNull(menu);
+            this.menu.entry = entry;
+            this.menu.cellCreator = Objects.requireNonNull(cellCreator);
+            this.menu.initCells();
+            this.topRenderer = Objects.requireNonNull(topRenderer);
+            this.topRenderer.entry = entry;
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, float delta) {
+            fill(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height, -6250336);
+            fill(bounds.x + 1, bounds.y + 1, bounds.x + bounds.width - 1, bounds.y + bounds.height - 1, -16777216);
+            topRenderer.render(mouseX, mouseY, bounds.x, bounds.y, bounds.width, bounds.height, delta);
+            if (menu.isExpended())
+                menu.render(mouseX, mouseY, bounds, delta);
+        }
+        
+        @Override
+        public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+            if (menu.isExpended())
+                return menu.mouseScrolled(double_1, double_2, double_3);
+            return false;
+        }
+        
+        public void lateRender(int mouseX, int mouseY, float delta) {
+            if (menu.isExpended())
+                menu.lateRender(mouseX, mouseY, delta);
+        }
+        
+        public int getMorePossibleHeight() {
+            if (menu.isExpended())
+                return menu.getHeight();
+            return -1;
+        }
+        
+        public R getValue() {
+            return topRenderer.getValue();
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Lists.newArrayList(topRenderer, menu);
+        }
+        
+        @Override
+        public boolean mouseClicked(double double_1, double double_2, int int_1) {
+            dontReFocus = false;
+            boolean b = super.mouseClicked(double_1, double_2, int_1);
+            if (dontReFocus) {
+                setFocused(null);
+                dontReFocus = false;
+            }
+            return b;
+        }
+    }
+    
+    public static abstract class DropdownMenuElement<R> extends AbstractParentElement {
+        @Deprecated @Nonnull private SelectionCellCreator<R> cellCreator;
+        @Deprecated @Nonnull private DropdownBoxEntry<R> entry;
+        
+        @Nonnull
+        public SelectionCellCreator<R> getCellCreator() {
+            return cellCreator;
+        }
+        
+        @Nonnull
+        public final DropdownBoxEntry<R> getEntry() {
+            return entry;
+        }
+        
+        @Nonnull
+        public abstract ImmutableList<R> getSelections();
+        
+        public abstract void initCells();
+        
+        public abstract void render(int mouseX, int mouseY, Rectangle rectangle, float delta);
+        
+        public abstract void lateRender(int mouseX, int mouseY, float delta);
+        
+        public abstract int getHeight();
+        
+        public final boolean isExpended() {
+            return (getEntry().selectionElement.getFocused() == getEntry().selectionElement.topRenderer || getEntry().selectionElement.getFocused() == getEntry().selectionElement.menu) && getEntry().getFocused() == getEntry().selectionElement && getEntry().getParent().getFocused() == getEntry();
+        }
+        
+        @Override
+        public abstract List<SelectionCellElement<R>> children();
+    }
+    
+    public static class DefaultDropdownMenuElement<R> extends DropdownMenuElement<R> {
+        @Nonnull protected ImmutableList<R> selections;
+        @Nonnull protected List<SelectionCellElement<R>> cells;
+        @Nonnull protected List<SelectionCellElement<R>> currentElements;
+        protected String lastSearchKeyword = "";
+        protected Rectangle lastRectangle;
+        protected boolean scrolling;
+        protected double scroll, target;
+        protected long start;
+        protected long duration;
+        protected RunSixtyTimesEverySec clamper = () -> {
+            target = clamp(target);
+            if (target < 0) {
+                target = target * getBounceBackMultiplier();
+            } else if (target > getMaxScrollPosition()) {
+                target = (target - getMaxScrollPosition()) * getBounceBackMultiplier() + getMaxScrollPosition();
+            } else
+                unregisterClamper();
+        };
+        
+        public DefaultDropdownMenuElement(@Nonnull ImmutableList<R> selections) {
+            this.selections = selections;
+            this.cells = Lists.newArrayList();
+            this.currentElements = Lists.newArrayList();
+        }
+        
+        protected void unregisterClamper() {
+            clamper.unregisterTick();
+        }
+        
+        public final double clamp(double v) {
+            return MathHelper.clamp(v, -SmoothScrollingSettings.CLAMP_EXTENSION, getMaxScrollPosition() + SmoothScrollingSettings.CLAMP_EXTENSION);
+        }
+        
+        public double getMaxScroll() {
+            return getCellCreator().getCellHeight() * currentElements.size();
+        }
+        
+        protected double getMaxScrollPosition() {
+            return Math.max(0, this.getMaxScroll() - (getHeight()));
+        }
+        
+        @Override
+        @Nonnull
+        public ImmutableList<R> getSelections() {
+            return selections;
+        }
+        
+        @Override
+        public void initCells() {
+            for(R selection : getSelections()) {
+                cells.add(getCellCreator().create(selection));
+            }
+            for(SelectionCellElement<R> cell : cells) {
+                cell.entry = getEntry();
+            }
+            search();
+        }
+        
+        public void search() {
+            currentElements.clear();
+            String keyword = this.lastSearchKeyword.toLowerCase();
+            for(SelectionCellElement<R> cell : cells) {
+                String key = cell.getSearchKey();
+                if (key == null || key.toLowerCase().contains(keyword))
+                    currentElements.add(cell);
+            }
+            if (!keyword.isEmpty()) {
+                Comparator<SelectionCellElement> c = Comparator.comparingDouble(i -> i.getSearchKey() == null ? Double.MAX_VALUE : similarity(i.getSearchKey(), keyword));
+                currentElements.sort(c.reversed());
+            }
+            scrollTo(0, false);
+        }
+        
+        protected int editDistance(String s1, String s2) {
+            s1 = s1.toLowerCase();
+            s2 = s2.toLowerCase();
+            
+            int[] costs = new int[s2.length() + 1];
+            for(int i = 0; i <= s1.length(); i++) {
+                int lastValue = i;
+                for(int j = 0; j <= s2.length(); j++) {
+                    if (i == 0)
+                        costs[j] = j;
+                    else {
+                        if (j > 0) {
+                            int newValue = costs[j - 1];
+                            if (s1.charAt(i - 1) != s2.charAt(j - 1))
+                                newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
+                            costs[j - 1] = lastValue;
+                            lastValue = newValue;
+                        }
+                    }
+                }
+                if (i > 0)
+                    costs[s2.length()] = lastValue;
+            }
+            return costs[s2.length()];
+        }
+        
+        protected double similarity(String s1, String s2) {
+            String longer = s1, shorter = s2;
+            if (s1.length() < s2.length()) { // longer should always have greater length
+                longer = s2;
+                shorter = s1;
+            }
+            int longerLength = longer.length();
+            if (longerLength == 0) {
+                return 1.0; /* both strings are zero length */
+            }
+            return (longerLength - editDistance(longer, shorter)) / (double) longerLength;
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, Rectangle rectangle, float delta) {
+            if (!getEntry().selectionElement.topRenderer.getSearchTerm().equals(lastSearchKeyword)) {
+                lastSearchKeyword = getEntry().selectionElement.topRenderer.getSearchTerm();
+                search();
+            }
+            updatePosition(delta);
+            lastRectangle = rectangle.clone();
+            lastRectangle.translate(0, -1);
+        }
+        
+        private void updatePosition(float delta) {
+            target = clamp(target);
+            if ((target < 0 || target > getMaxScrollPosition()) && !clamper.isRegistered())
+                clamper.registerTick();
+            if (!Precision.almostEquals(scroll, target, Precision.FLOAT_EPSILON))
+                scroll = (float) Interpolation.expoEase(scroll, target, Math.min((System.currentTimeMillis() - start) / ((double) duration), 1));
+            else
+                scroll = target;
+        }
+        
+        @Override
+        public void lateRender(int mouseX, int mouseY, float delta) {
+            int last10Height = getHeight();
+            int cWidth = getCellCreator().getCellWidth();
+            fill(lastRectangle.x, lastRectangle.y + lastRectangle.height, lastRectangle.x + cWidth, lastRectangle.y + lastRectangle.height + last10Height + 1, -6250336);
+            fill(lastRectangle.x + 1, lastRectangle.y + lastRectangle.height + 1, lastRectangle.x + cWidth - 1, lastRectangle.y + lastRectangle.height + last10Height, -16777216);
+            RenderSystem.pushMatrix();
+            Window window = MinecraftClient.getInstance().getWindow();
+            int sw = window.getWidth();
+            int sh = window.getHeight();
+            float dw = window.getScaledWidth();
+            float dh = window.getScaledHeight();
+            {
+                int yyy = Math.max(lastRectangle.y + lastRectangle.height + 1, getEntry().getParent().top);
+                int x = Math.round(sw * (lastRectangle.x / dw));
+                int y = Math.round(sh * (yyy / dh));
+                int ww = Math.round(sw * ((cWidth - 6) / dw));
+                int hh = Math.round(sh * ((Math.min(last10Height - 1 - yyy + (lastRectangle.y + lastRectangle.height + 1), getEntry().getParent().bottom - yyy)) / dh));
+                GL11.glEnable(GL11.GL_SCISSOR_TEST);
+                GL11.glScissor(x, sh - hh - y, ww, hh);
+                double yy = lastRectangle.y + lastRectangle.height - scroll;
+                for(SelectionCellElement<R> cell : currentElements) {
+                    if (yy + getCellCreator().getCellHeight() >= lastRectangle.y + lastRectangle.height && yy <= lastRectangle.y + lastRectangle.height + last10Height + 1)
+                        cell.render(mouseX, mouseY, lastRectangle.x, (int) yy, getMaxScrollPosition() > 6 ? getCellCreator().getCellWidth() - 6 : getCellCreator().getCellWidth(), getCellCreator().getCellHeight(), delta);
+                    else
+                        cell.dontRender(delta);
+                    yy += getCellCreator().getCellHeight();
+                }
+                GL11.glDisable(GL11.GL_SCISSOR_TEST);
+            }
+            if (currentElements.isEmpty()) {
+                TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+                String s = I18n.translate("text.cloth-config.dropdown.value.unknown");
+                textRenderer.drawWithShadow(s, lastRectangle.x + getCellCreator().getCellWidth() / 2 - textRenderer.getStringWidth(s) / 2, lastRectangle.y + lastRectangle.height + 3, -1);
+            }
+            {
+                int y = Math.round(sh * (getEntry().getParent().top / dh));
+                int hh = Math.round(sh * ((getEntry().getParent().bottom - getEntry().getParent().top) / dh));
+                GL11.glEnable(GL11.GL_SCISSOR_TEST);
+                GL11.glScissor(0, sh - hh - y, sw, hh);
+            }
+            if (getMaxScrollPosition() > 6) {
+                RenderSystem.disableTexture();
+                int scrollbarPositionMinX = lastRectangle.x + getCellCreator().getCellWidth() - 6;
+                int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
+                int height = (int) (((last10Height) * (last10Height)) / this.getMaxScrollPosition());
+                height = MathHelper.clamp(height, 32, last10Height - 8);
+                height -= Math.min((scroll < 0 ? (int) -scroll : scroll > getMaxScrollPosition() ? (int) scroll - getMaxScrollPosition() : 0), height * .95);
+                height = Math.max(10, height);
+                int minY = (int) Math.min(Math.max((int) scroll * (last10Height - height) / getMaxScrollPosition() + (lastRectangle.y + lastRectangle.height + 1), (lastRectangle.y + lastRectangle.height + 1)), (lastRectangle.y + lastRectangle.height + 1 + last10Height) - height);
+                
+                int bottomc = new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height).contains(PointHelper.fromMouse()) ? 168 : 128;
+                int topc = new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height).contains(PointHelper.fromMouse()) ? 222 : 172;
+                
+                Tessellator tessellator = Tessellator.getInstance();
+                BufferBuilder buffer = tessellator.getBufferBuilder();
+                
+                // Bottom
+                buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+                buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).texture(0, 1).color(bottomc, bottomc, bottomc, 255).next();
+                buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).texture(1, 1).color(bottomc, bottomc, bottomc, 255).next();
+                buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).texture(1, 0).color(bottomc, bottomc, bottomc, 255).next();
+                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(bottomc, bottomc, bottomc, 255).next();
+                tessellator.draw();
+                
+                // Top
+                buffer.begin(7, VertexFormats.POSITION_UV_COLOR);
+                buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).texture(0, 1).color(topc, topc, topc, 255).next();
+                buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).texture(1, 1).color(topc, topc, topc, 255).next();
+                buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).texture(1, 0).color(topc, topc, topc, 255).next();
+                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(topc, topc, topc, 255).next();
+                tessellator.draw();
+                RenderSystem.enableTexture();
+            }
+            GL11.glDisable(GL11.GL_SCISSOR_TEST);
+            RenderSystem.popMatrix();
+        }
+        
+        @Override
+        public int getHeight() {
+            return Math.max(Math.min(getCellCreator().getDropBoxMaxHeight(), (int) getMaxScroll()), 14);
+        }
+        
+        @Override
+        public boolean isMouseOver(double mouseX, double mouseY) {
+            return isExpended() && mouseX >= lastRectangle.x && mouseX <= lastRectangle.x + getCellCreator().getCellWidth() && mouseY >= lastRectangle.y + lastRectangle.height && mouseY <= lastRectangle.y + lastRectangle.height + getHeight() + 1;
+        }
+        
+        @Override
+        public boolean mouseDragged(double double_1, double double_2, int int_1, double double_3, double double_4) {
+            if (!isExpended())
+                return false;
+            if (int_1 == 0 && this.scrolling) {
+                if (double_2 < (double) lastRectangle.y + lastRectangle.height) {
+                    scrollTo(0, false);
+                } else if (double_2 > (double) lastRectangle.y + lastRectangle.height + getHeight()) {
+                    scrollTo(getMaxScrollPosition(), false);
+                } else {
+                    double double_5 = (double) Math.max(1, this.getMaxScrollPosition());
+                    int int_2 = getHeight();
+                    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.offset(double_4 * double_6, false);
+                }
+                target = MathHelper.clamp(target, 0, getMaxScrollPosition());
+                return true;
+            }
+            return false;
+        }
+        
+        @Override
+        public boolean mouseScrolled(double mouseX, double mouseY, double double_3) {
+            if (isMouseOver(mouseX, mouseY)) {
+                offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+                return true;
+            }
+            return false;
+        }
+        
+        protected void updateScrollingState(double double_1, double double_2, int int_1) {
+            this.scrolling = isExpended() && lastRectangle != null && int_1 == 0 && double_1 >= (double) lastRectangle.x + getCellCreator().getCellWidth() - 6 && double_1 < (double) (lastRectangle.x + getCellCreator().getCellWidth());
+        }
+        
+        @Override
+        public boolean mouseClicked(double double_1, double double_2, int int_1) {
+            if (!isExpended())
+                return false;
+            updateScrollingState(double_1, double_2, int_1);
+            return super.mouseClicked(double_1, double_2, int_1) || scrolling;
+        }
+        
+        public void offset(double value, boolean animated) {
+            scrollTo(target + value, animated);
+        }
+        
+        public void scrollTo(double value, boolean animated) {
+            scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
+        }
+        
+        public void scrollTo(double value, boolean animated, long duration) {
+            target = clamp(value);
+            
+            if (animated) {
+                start = System.currentTimeMillis();
+                this.duration = duration;
+            } else
+                scroll = target;
+        }
+        
+        @Override
+        public List<SelectionCellElement<R>> children() {
+            return currentElements;
+        }
+    }
+    
+    public static abstract class SelectionCellCreator<R> {
+        public abstract SelectionCellElement<R> create(R selection);
+        
+        public abstract int getCellHeight();
+        
+        public abstract int getDropBoxMaxHeight();
+        
+        public int getCellWidth() {
+            return 132;
+        }
+    }
+    
+    public static class DefaultSelectionCellCreator<R> extends SelectionCellCreator<R> {
+        protected Function<R, String> toStringFunction;
+        
+        public DefaultSelectionCellCreator(Function<R, String> toStringFunction) {
+            this.toStringFunction = toStringFunction;
+        }
+        
+        public DefaultSelectionCellCreator() {
+            this(Object::toString);
+        }
+        
+        @Override
+        public SelectionCellElement<R> create(R selection) {
+            return new DefaultSelectionCellElement<>(selection, toStringFunction);
+        }
+        
+        @Override
+        public int getCellHeight() {
+            return 14;
+        }
+        
+        @Override
+        public int getDropBoxMaxHeight() {
+            return getCellHeight() * 7;
+        }
+    }
+    
+    public static abstract class SelectionCellElement<R> extends AbstractParentElement {
+        @Deprecated @Nonnull private DropdownBoxEntry<R> entry;
+        
+        @Nonnull
+        public final DropdownBoxEntry<R> getEntry() {
+            return entry;
+        }
+        
+        public abstract void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta);
+        
+        public abstract void dontRender(float delta);
+        
+        @Nullable
+        public abstract String getSearchKey();
+        
+        @Nullable
+        public abstract R getSelection();
+    }
+    
+    public static class DefaultSelectionCellElement<R> extends SelectionCellElement<R> {
+        protected R r;
+        protected int x;
+        protected int y;
+        protected int width;
+        protected int height;
+        protected boolean rendering;
+        protected Function<R, String> toStringFunction;
+        
+        public DefaultSelectionCellElement(R r, Function<R, String> toStringFunction) {
+            this.r = r;
+            this.toStringFunction = toStringFunction;
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+            rendering = true;
+            this.x = x;
+            this.y = y;
+            this.width = width;
+            this.height = height;
+            boolean b = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
+            if (b)
+                fill(x + 1, y + 1, x + width - 1, y + height - 1, -15132391);
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(toStringFunction.apply(r), x + 6, y + 3, b ? 16777215 : 8947848);
+        }
+        
+        @Override
+        public void dontRender(float delta) {
+            rendering = false;
+        }
+        
+        @Nullable
+        @Override
+        public String getSearchKey() {
+            return toStringFunction.apply(r);
+        }
+        
+        @Nullable
+        @Override
+        public R getSelection() {
+            return r;
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.emptyList();
+        }
+        
+        @Override
+        public boolean mouseClicked(double mouseX, double mouseY, int int_1) {
+            boolean b = rendering && mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
+            if (b) {
+                getEntry().selectionElement.topRenderer.setValue(r);
+                getEntry().selectionElement.setFocused(null);
+                getEntry().selectionElement.dontReFocus = true;
+                return true;
+            }
+            return false;
+        }
+    }
+    
+    public static abstract class SelectionTopCellElement<R> extends AbstractParentElement {
+        @Deprecated private Optional<String> error;
+        @Deprecated private DropdownBoxEntry<R> entry;
+        
+        public abstract R getValue();
+        
+        public abstract void setValue(R value);
+        
+        public abstract String getSearchTerm();
+        
+        public abstract Optional<String> getError();
+        
+        public final Optional<String> getConfigError() {
+            return error;
+        }
+        
+        public DropdownBoxEntry<R> getParent() {
+            return entry;
+        }
+        
+        public final boolean hasConfigError() {
+            return error.isPresent();
+        }
+        
+        public final int getPreferredTextColor() {
+            return error.isPresent() ? 16733525 : 16777215;
+        }
+        
+        public void selectFirstRecommendation() {
+            List<SelectionCellElement<R>> children = getParent().selectionElement.menu.children();
+            for(SelectionCellElement<R> child : children) {
+                if (child.getSelection() != null) {
+                    setValue(child.getSelection());
+                    getParent().selectionElement.setFocused(null);
+                    break;
+                }
+            }
+        }
+        
+        public abstract void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta);
+    }
+    
+    public static class DefaultSelectionTopCellElement<R> extends SelectionTopCellElement<R> {
+        protected TextFieldWidget textFieldWidget;
+        protected Function<String, R> toObjectFunction;
+        protected Function<R, String> toStringFunction;
+        protected R value;
+        
+        public DefaultSelectionTopCellElement(R value, Function<String, R> toObjectFunction, Function<R, String> toStringFunction) {
+            this.value = Objects.requireNonNull(value);
+            this.toObjectFunction = Objects.requireNonNull(toObjectFunction);
+            this.toStringFunction = Objects.requireNonNull(toStringFunction);
+            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(DefaultSelectionTopCellElement.this.getParent().getParent().getFocused() == DefaultSelectionTopCellElement.this.getParent() && DefaultSelectionTopCellElement.this.getParent().getFocused() == DefaultSelectionTopCellElement.this.getParent().selectionElement && DefaultSelectionTopCellElement.this.getParent().selectionElement.getFocused() == DefaultSelectionTopCellElement.this && DefaultSelectionTopCellElement.this.getFocused() == this);
+                    super.render(int_1, int_2, float_1);
+                    setFocused(f);
+                }
+                
+                @Override
+                public boolean keyPressed(int int_1, int int_2, int int_3) {
+                    if (int_1 == 257 || int_1 == 335) {
+                        DefaultSelectionTopCellElement.this.selectFirstRecommendation();
+                        return true;
+                    }
+                    return super.keyPressed(int_1, int_2, int_3);
+                }
+            };
+            textFieldWidget.setHasBorder(false);
+            textFieldWidget.setMaxLength(999999);
+            textFieldWidget.setText(toStringFunction.apply(value));
+            textFieldWidget.setChangedListener(s -> {
+                if (!toStringFunction.apply(value).equals(s))
+                    getParent().getScreen().setEdited(true, getParent().isRequiresRestart());
+            });
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+            textFieldWidget.x = x + 4;
+            textFieldWidget.y = y + 6;
+            textFieldWidget.setWidth(width - 8);
+            textFieldWidget.setEditable(getParent().isEditable());
+            textFieldWidget.setEditableColor(getPreferredTextColor());
+            textFieldWidget.render(mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public R getValue() {
+            if (hasConfigError())
+                return value;
+            return toObjectFunction.apply(textFieldWidget.getText());
+        }
+        
+        @Override
+        public void setValue(R value) {
+            textFieldWidget.setText(toStringFunction.apply(value));
+            textFieldWidget.setCursor(0);
+        }
+        
+        @Override
+        public String getSearchTerm() {
+            return textFieldWidget.getText();
+        }
+        
+        @Override
+        public Optional<String> getError() {
+            if (toObjectFunction.apply(textFieldWidget.getText()) != null)
+                return Optional.empty();
+            return Optional.of("Invalid Value!");
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(textFieldWidget);
+        }
+    }
+}

+ 2 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/SelectionListEntry.java

@@ -8,6 +8,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.util.Window;
 
+import javax.annotation.Nonnull;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -119,6 +120,7 @@ public class SelectionListEntry<T> extends TooltipListEntry<T> {
     }
     
     public static interface Translatable {
+        @Nonnull
         String getKey();
     }
     

+ 8 - 4
src/main/java/me/shedaniel/clothconfig2/gui/entries/TooltipListEntry.java

@@ -4,19 +4,22 @@ import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 import me.shedaniel.clothconfig2.api.QueuedTooltip;
 import me.shedaniel.math.api.Point;
 
+import javax.annotation.Nullable;
 import java.util.Optional;
 import java.util.function.Supplier;
 
 public abstract class TooltipListEntry<T> extends AbstractConfigListEntry<T> {
     
-    private Supplier<Optional<String[]>> tooltipSupplier;
+    @Nullable private Supplier<Optional<String[]>> tooltipSupplier;
     
     @Deprecated
-    public TooltipListEntry(String fieldName, Supplier<Optional<String[]>> tooltipSupplier) {
+    public TooltipListEntry(String fieldName, @Nullable Supplier<Optional<String[]>> tooltipSupplier) {
         this(fieldName, tooltipSupplier, false);
     }
     
-    public TooltipListEntry(String fieldName, Supplier<Optional<String[]>> tooltipSupplier, boolean requiresRestart) {
+    @Deprecated
+    public TooltipListEntry(String fieldName,
+            @Nullable Supplier<Optional<String[]>> tooltipSupplier, boolean requiresRestart) {
         super(fieldName, requiresRestart);
         this.tooltipSupplier = tooltipSupplier;
     }
@@ -40,11 +43,12 @@ public abstract class TooltipListEntry<T> extends AbstractConfigListEntry<T> {
         return Optional.empty();
     }
     
+    @Nullable
     public Supplier<Optional<String[]>> getTooltipSupplier() {
         return tooltipSupplier;
     }
     
-    public void setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+    public void setTooltipSupplier(@Nullable Supplier<Optional<String[]>> tooltipSupplier) {
         this.tooltipSupplier = tooltipSupplier;
     }
     

+ 41 - 10
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicEntryListWidget.java

@@ -13,8 +13,10 @@ 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.client.util.Window;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
 
 import java.util.AbstractList;
 import java.util.ArrayList;
@@ -27,12 +29,12 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     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;
+    public int width;
+    public int height;
+    public int top;
+    public int bottom;
+    public int right;
+    public int left;
     protected boolean verticallyCenter = true;
     protected int yDrag = -2;
     protected boolean visible = true;
@@ -42,6 +44,7 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     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;
@@ -138,10 +141,16 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     }
     
     protected int getMaxScrollPosition() {
+        List<Integer> list = new ArrayList<>();
         int i = headerHeight;
-        for(E entry : entries)
+        for(E entry : entries) {
             i += entry.getItemHeight();
-        return i;
+            if (entry.getMorePossibleHeight() >= 0) {
+                list.add(i + entry.getMorePossibleHeight());
+            }
+        }
+        list.add(i);
+        return list.stream().max((a, b) -> Integer.compare(a, b)).orElse(0);
     }
     
     protected void clickedHeader(int int_1, int int_2) {
@@ -177,8 +186,19 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
         int startY = this.top + 4 - (int) this.getScroll();
         if (this.renderSelection)
             this.renderHeader(rowLeft, startY, tessellator);
-        
+        Window window = client.getWindow();
+        int sw = window.getWidth();
+        int sh = window.getHeight();
+        float dw = window.getScaledWidth();
+        float dh = window.getScaledHeight();
+        int x = Math.round(sw * (this.left / dw));
+        int y = Math.round(sh * (this.top / dh));
+        int ww = Math.round(sw * (this.width / dw));
+        int hh = Math.round(sh * (this.height / dh));
+        GL11.glEnable(GL11.GL_SCISSOR_TEST);
+        GL11.glScissor(x, sh - hh - y, ww, hh);
         this.renderList(rowLeft, startY, mouseX, mouseY, delta);
+        GL11.glDisable(GL11.GL_SCISSOR_TEST);
         RenderSystem.disableDepthTest();
         this.renderHoleBackground(0, this.top, 255, 255);
         this.renderHoleBackground(this.bottom, this.height, 255, 255);
@@ -337,6 +357,11 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     }
     
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        for(E entry : entries) {
+            if (entry.mouseScrolled(double_1, double_2, double_3)) {
+                return true;
+            }
+        }
         this.capYPosition(this.getScroll() - double_3 * (double) (getMaxScroll() / getItemCount()) / 2.0D);
         return true;
     }
@@ -457,8 +482,9 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     }
     
     public static final class SmoothScrollingSettings {
-        private SmoothScrollingSettings() {}
         public static final double CLAMP_EXTENSION = 200;
+        
+        private SmoothScrollingSettings() {}
     }
     
     @SuppressWarnings("deprecation")
@@ -484,6 +510,11 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
         }
         
         public abstract int getItemHeight();
+        
+        @Deprecated
+        public int getMorePossibleHeight() {
+            return -1;
+        }
     }
     
     @Environment(EnvType.CLIENT)

+ 6 - 1
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicNewSmoothScrollingEntryListWidget.java

@@ -88,9 +88,14 @@ public abstract class DynamicNewSmoothScrollingEntryListWidget<E extends Dynamic
     
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        for(E entry : children()) {
+            if (entry.mouseScrolled(double_1, double_2, double_3)) {
+                return true;
+            }
+        }
         if (!smoothScrolling) {
             scroll += 16 * -double_3;
-            this.scroll = MathHelper.clamp(double_1, 0.0D, (double) this.getMaxScroll());
+            this.scroll = MathHelper.clamp(double_3, 0.0D, (double) this.getMaxScroll());
             return true;
         }
         offset(ClothConfigInitializer.getScrollStep() * -double_3, true);

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

@@ -101,6 +101,11 @@ public abstract class DynamicSmoothScrollingEntryListWidget<E extends DynamicEnt
     
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        for(E entry : children()) {
+            if (entry.mouseScrolled(double_1, double_2, double_3)) {
+                return true;
+            }
+        }
         if (!smoothScrolling) {
             this.scrollVelocity = 0d;
             scroll += 16 * -double_3;

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

@@ -2,6 +2,9 @@ package me.shedaniel.clothconfig2.impl;
 
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionCellCreator;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionTopCellElement;
 import me.shedaniel.clothconfig2.impl.builders.*;
 
 import java.util.List;
@@ -135,4 +138,9 @@ public class ConfigEntryBuilderImpl implements ConfigEntryBuilder {
         return new LongSliderBuilder(resetButtonKey, fieldNameKey, value, min, max);
     }
     
+    @Override
+    public <T> DropdownMenuBuilder<T> startDropdownMenu(String fieldNameKey, SelectionTopCellElement<T> topCellElement, SelectionCellCreator<T> cellCreator) {
+        return new DropdownMenuBuilder(resetButtonKey, fieldNameKey, topCellElement, cellCreator);
+    }
+    
 }

+ 17 - 10
src/main/java/me/shedaniel/clothconfig2/impl/builders/BooleanToggleBuilder.java

@@ -2,7 +2,8 @@ package me.shedaniel.clothconfig2.impl.builders;
 
 import me.shedaniel.clothconfig2.gui.entries.BooleanListEntry;
 
-import java.util.Objects;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -10,17 +11,17 @@ import java.util.function.Supplier;
 
 public class BooleanToggleBuilder extends FieldBuilder<Boolean, BooleanListEntry> {
     
-    private Consumer<Boolean> saveConsumer = null;
-    private Function<Boolean, Optional<String[]>> tooltipSupplier = bool -> Optional.empty();
+    @Nullable private Consumer<Boolean> saveConsumer = null;
+    @Nonnull private Function<Boolean, Optional<String[]>> tooltipSupplier = bool -> Optional.empty();
     private boolean value;
-    private Function<Boolean, String> yesNoTextSupplier = bool -> bool ? "§aYes" : "§cNo";
+    @Nullable private Function<Boolean, String> yesNoTextSupplier = null;
     
     public BooleanToggleBuilder(String resetButtonKey, String fieldNameKey, boolean value) {
         super(resetButtonKey, fieldNameKey);
         this.value = value;
     }
     
-    public BooleanToggleBuilder setErrorSupplier(Function<Boolean, Optional<String>> errorSupplier) {
+    public BooleanToggleBuilder setErrorSupplier(@Nullable Function<Boolean, Optional<String>> errorSupplier) {
         this.errorSupplier = errorSupplier;
         return this;
     }
@@ -45,12 +46,12 @@ public class BooleanToggleBuilder extends FieldBuilder<Boolean, BooleanListEntry
         return this;
     }
     
-    public BooleanToggleBuilder setTooltipSupplier(Function<Boolean, Optional<String[]>> tooltipSupplier) {
+    public BooleanToggleBuilder setTooltipSupplier(@Nonnull Function<Boolean, Optional<String[]>> tooltipSupplier) {
         this.tooltipSupplier = tooltipSupplier;
         return this;
     }
     
-    public BooleanToggleBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+    public BooleanToggleBuilder setTooltipSupplier(@Nonnull Supplier<Optional<String[]>> tooltipSupplier) {
         this.tooltipSupplier = bool -> tooltipSupplier.get();
         return this;
     }
@@ -60,13 +61,17 @@ public class BooleanToggleBuilder extends FieldBuilder<Boolean, BooleanListEntry
         return this;
     }
     
-    public BooleanToggleBuilder setTooltip(String... tooltip) {
+    public BooleanToggleBuilder setTooltip(@Nullable String... tooltip) {
         this.tooltipSupplier = bool -> Optional.ofNullable(tooltip);
         return this;
     }
     
-    public BooleanToggleBuilder setYesNoTextSupplier(Function<Boolean, String> yesNoTextSupplier) {
-        Objects.requireNonNull(yesNoTextSupplier);
+    @Nullable
+    public Function<Boolean, String> getYesNoTextSupplier() {
+        return yesNoTextSupplier;
+    }
+    
+    public BooleanToggleBuilder setYesNoTextSupplier(@Nullable Function<Boolean, String> yesNoTextSupplier) {
         this.yesNoTextSupplier = yesNoTextSupplier;
         return this;
     }
@@ -76,6 +81,8 @@ public class BooleanToggleBuilder extends FieldBuilder<Boolean, BooleanListEntry
         BooleanListEntry entry = new BooleanListEntry(getFieldNameKey(), value, getResetButtonKey(), defaultValue, saveConsumer, null, isRequireRestart()) {
             @Override
             public String getYesNoText(boolean bool) {
+                if (yesNoTextSupplier == null)
+                    return super.getYesNoText(bool);
                 return yesNoTextSupplier.apply(bool);
             }
         };

+ 521 - 0
src/main/java/me/shedaniel/clothconfig2/impl/builders/DropdownMenuBuilder.java

@@ -0,0 +1,521 @@
+package me.shedaniel.clothconfig2.impl.builders;
+
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.DefaultSelectionCellCreator;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.DefaultSelectionTopCellElement;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionCellCreator;
+import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry.SelectionTopCellElement;
+import net.minecraft.block.Block;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.item.ItemRenderer;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.Registry;
+
+import javax.annotation.Nonnull;
+import java.util.Collections;
+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 DropdownMenuBuilder<T> extends FieldBuilder<T, DropdownBoxEntry<T>> {
+    protected SelectionTopCellElement<T> topCellElement;
+    protected SelectionCellCreator<T> cellCreator;
+    protected Function<T, Optional<String[]>> tooltipSupplier = str -> Optional.empty();
+    protected Consumer<T> saveConsumer = null;
+    protected Iterable<T> selections = Collections.emptyList();
+    
+    public DropdownMenuBuilder(String resetButtonKey, String fieldNameKey, SelectionTopCellElement<T> topCellElement, SelectionCellCreator<T> cellCreator) {
+        super(resetButtonKey, fieldNameKey);
+        this.topCellElement = Objects.requireNonNull(topCellElement);
+        this.cellCreator = Objects.requireNonNull(cellCreator);
+    }
+    
+    public DropdownMenuBuilder setSelections(Iterable<T> selections) {
+        this.selections = selections;
+        return this;
+    }
+    
+    public DropdownMenuBuilder setDefaultValue(Supplier<T> defaultValue) {
+        this.defaultValue = defaultValue;
+        return this;
+    }
+    
+    public DropdownMenuBuilder setDefaultValue(T defaultValue) {
+        this.defaultValue = () -> Objects.requireNonNull(defaultValue);
+        return this;
+    }
+    
+    public DropdownMenuBuilder setSaveConsumer(Consumer<T> saveConsumer) {
+        this.saveConsumer = saveConsumer;
+        return this;
+    }
+    
+    public DropdownMenuBuilder setTooltipSupplier(Supplier<Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = str -> tooltipSupplier.get();
+        return this;
+    }
+    
+    public DropdownMenuBuilder setTooltipSupplier(Function<T, Optional<String[]>> tooltipSupplier) {
+        this.tooltipSupplier = tooltipSupplier;
+        return this;
+    }
+    
+    public DropdownMenuBuilder setTooltip(Optional<String[]> tooltip) {
+        this.tooltipSupplier = str -> tooltip;
+        return this;
+    }
+    
+    public DropdownMenuBuilder setTooltip(String... tooltip) {
+        this.tooltipSupplier = str -> Optional.ofNullable(tooltip);
+        return this;
+    }
+    
+    public DropdownMenuBuilder requireRestart() {
+        requireRestart(true);
+        return this;
+    }
+    
+    public DropdownMenuBuilder setErrorSupplier(Function<T, Optional<String>> errorSupplier) {
+        this.errorSupplier = errorSupplier;
+        return this;
+    }
+    
+    @Nonnull
+    @Override
+    public DropdownBoxEntry<T> build() {
+        DropdownBoxEntry<T> entry = new DropdownBoxEntry<T>(getFieldNameKey(), getResetButtonKey(), null, isRequireRestart(), defaultValue, saveConsumer, selections, topCellElement, cellCreator);
+        entry.setTooltipSupplier(() -> tooltipSupplier.apply(entry.getValue()));
+        if (errorSupplier != null)
+            entry.setErrorSupplier(() -> errorSupplier.apply(entry.getValue()));
+        return entry;
+    }
+    
+    public static class TopCellElementBuilder {
+        public static final Function<String, Identifier> IDENTIFIER_FUNCTION = str -> {
+            try {
+                return new Identifier(str);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        };
+        public static final Function<String, Identifier> ITEM_IDENTIFIER_FUNCTION = str -> {
+            try {
+                Identifier identifier = new Identifier(str);
+                if (Registry.ITEM.getOrEmpty(identifier).isPresent())
+                    return identifier;
+            } catch (Exception ignored) {
+            }
+            return null;
+        };
+        public static final Function<String, Identifier> BLOCK_IDENTIFIER_FUNCTION = str -> {
+            try {
+                Identifier identifier = new Identifier(str);
+                if (Registry.BLOCK.getOrEmpty(identifier).isPresent())
+                    return identifier;
+            } catch (Exception ignored) {
+            }
+            return null;
+        };
+        public static final Function<String, Item> ITEM_FUNCTION = str -> {
+            try {
+                return Registry.ITEM.getOrEmpty(new Identifier(str)).get();
+            } catch (Exception ignored) {
+            }
+            return null;
+        };
+        public static final Function<String, Block> BLOCK_FUNCTION = str -> {
+            try {
+                return Registry.BLOCK.getOrEmpty(new Identifier(str)).get();
+            } catch (Exception ignored) {
+            }
+            return null;
+        };
+        private static final ItemStack BARRIER = new ItemStack(Items.BARRIER);
+    
+        public static <T> SelectionTopCellElement<T> of(T value, Function<String, T> toObjectFunction) {
+            return of(value, toObjectFunction, Object::toString);
+        }
+        
+        public static <T> SelectionTopCellElement<T> of(T value, Function<String, T> toObjectFunction, Function<T, String> toStringFunction) {
+            return new DefaultSelectionTopCellElement<>(value, toObjectFunction, toStringFunction);
+        }
+        
+        public static SelectionTopCellElement<Identifier> ofItemIdentifier(Item item) {
+            return new DefaultSelectionTopCellElement<Identifier>(Registry.ITEM.getId(item), ITEM_IDENTIFIER_FUNCTION, Identifier::toString) {
+                @Override
+                public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                    textFieldWidget.x = x + 4;
+                    textFieldWidget.y = y + 6;
+                    textFieldWidget.setWidth(width - 4 - 20);
+                    textFieldWidget.setEditable(getParent().isEditable());
+                    textFieldWidget.setEditableColor(getPreferredTextColor());
+                    textFieldWidget.render(mouseX, mouseY, delta);
+                    ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                    ItemStack stack = hasConfigError() ? BARRIER : new ItemStack(Registry.ITEM.get(getValue()));
+                    itemRenderer.renderGuiItemIcon(stack, x + width - 18, y + 2);
+                }
+            };
+        }
+        
+        public static SelectionTopCellElement<Identifier> ofBlockIdentifier(Block block) {
+            return new DefaultSelectionTopCellElement<Identifier>(Registry.BLOCK.getId(block), BLOCK_IDENTIFIER_FUNCTION, Identifier::toString) {
+                @Override
+                public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                    textFieldWidget.x = x + 4;
+                    textFieldWidget.y = y + 6;
+                    textFieldWidget.setWidth(width - 4 - 20);
+                    textFieldWidget.setEditable(getParent().isEditable());
+                    textFieldWidget.setEditableColor(getPreferredTextColor());
+                    textFieldWidget.render(mouseX, mouseY, delta);
+                    ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                    ItemStack stack = hasConfigError() ? BARRIER : new ItemStack(Registry.BLOCK.get(getValue()));
+                    itemRenderer.renderGuiItemIcon(stack, x + width - 18, y + 2);
+                }
+            };
+        }
+        
+        public static SelectionTopCellElement<Item> ofItemObject(Item item) {
+            return new DefaultSelectionTopCellElement<Item>(item, ITEM_FUNCTION, i -> Registry.ITEM.getId(i).toString()) {
+                @Override
+                public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                    textFieldWidget.x = x + 4;
+                    textFieldWidget.y = y + 6;
+                    textFieldWidget.setWidth(width - 4 - 20);
+                    textFieldWidget.setEditable(getParent().isEditable());
+                    textFieldWidget.setEditableColor(getPreferredTextColor());
+                    textFieldWidget.render(mouseX, mouseY, delta);
+                    ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                    ItemStack stack = hasConfigError() ? BARRIER : new ItemStack(getValue());
+                    itemRenderer.renderGuiItemIcon(stack, x + width - 18, y + 2);
+                }
+            };
+        }
+        
+        public static SelectionTopCellElement<Block> ofBlockObject(Block block) {
+            return new DefaultSelectionTopCellElement<Block>(block, BLOCK_FUNCTION, i -> Registry.BLOCK.getId(i).toString()) {
+                @Override
+                public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                    textFieldWidget.x = x + 4;
+                    textFieldWidget.y = y + 6;
+                    textFieldWidget.setWidth(width - 4 - 20);
+                    textFieldWidget.setEditable(getParent().isEditable());
+                    textFieldWidget.setEditableColor(getPreferredTextColor());
+                    textFieldWidget.render(mouseX, mouseY, delta);
+                    ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                    ItemStack stack = hasConfigError() ? BARRIER : new ItemStack(getValue());
+                    itemRenderer.renderGuiItemIcon(stack, x + width - 18, y + 2);
+                }
+            };
+        }
+    }
+    
+    public static class CellCreatorBuilder {
+        public static <T> SelectionCellCreator<T> of() {
+            return new DefaultSelectionCellCreator<>();
+        }
+        
+        public static <T> SelectionCellCreator<T> of(Function<T, String> toStringFunction) {
+            return new DefaultSelectionCellCreator<>(toStringFunction);
+        }
+        
+        public static <T> SelectionCellCreator<T> ofWidth(int cellWidth) {
+            return new DefaultSelectionCellCreator<T>() {
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+            };
+        }
+        
+        public static <T> SelectionCellCreator<T> ofWidth(int cellWidth, Function<T, String> toStringFunction) {
+            return new DefaultSelectionCellCreator<T>(toStringFunction) {
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+            };
+        }
+        
+        public static <T> SelectionCellCreator<T> ofCellCount(int maxItems) {
+            return new DefaultSelectionCellCreator<T>() {
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static <T> SelectionCellCreator<T> ofCellCount(int maxItems, Function<T, String> toStringFunction) {
+            return new DefaultSelectionCellCreator<T>(toStringFunction) {
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static <T> SelectionCellCreator<T> of(int cellWidth, int maxItems) {
+            return new DefaultSelectionCellCreator<T>() {
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static <T> SelectionCellCreator<T> of(int cellWidth, int maxItems, Function<T, String> toStringFunction) {
+            return new DefaultSelectionCellCreator<T>(toStringFunction) {
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static <T> SelectionCellCreator<T> of(int cellHeight, int cellWidth, int maxItems) {
+            return new DefaultSelectionCellCreator<T>() {
+                @Override
+                public int getCellHeight() {
+                    return cellHeight;
+                }
+                
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static <T> SelectionCellCreator<T> of(int cellHeight, int cellWidth, int maxItems, Function<T, String> toStringFunction) {
+            return new DefaultSelectionCellCreator<T>(toStringFunction) {
+                @Override
+                public int getCellHeight() {
+                    return cellHeight;
+                }
+                
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static SelectionCellCreator<Identifier> ofItemIdentifier() {
+            return ofItemIdentifier(20, 146, 7);
+        }
+        
+        public static SelectionCellCreator<Identifier> ofItemIdentifier(int maxItems) {
+            return ofItemIdentifier(20, 146, maxItems);
+        }
+        
+        public static SelectionCellCreator<Identifier> ofItemIdentifier(int cellHeight, int cellWidth, int maxItems) {
+            return new DefaultSelectionCellCreator<Identifier>() {
+                @Override
+                public DropdownBoxEntry.SelectionCellElement<Identifier> create(Identifier selection) {
+                    ItemStack s = new ItemStack(Registry.ITEM.get(selection));
+                    return new DropdownBoxEntry.DefaultSelectionCellElement<Identifier>(selection, toStringFunction) {
+                        @Override
+                        public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                            rendering = true;
+                            this.x = x;
+                            this.y = y;
+                            this.width = width;
+                            this.height = height;
+                            boolean b = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
+                            if (b)
+                                fill(x + 1, y + 1, x + width - 1, y + height - 1, -15132391);
+                            MinecraftClient.getInstance().textRenderer.drawWithShadow(toStringFunction.apply(r), x + 6 + 18, y + 6, b ? 16777215 : 8947848);
+                            ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                            itemRenderer.renderGuiItemIcon(s, x + 4, y + 2);
+                        }
+                    };
+                }
+                
+                @Override
+                public int getCellHeight() {
+                    return cellHeight;
+                }
+                
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        
+        public static SelectionCellCreator<Identifier> ofBlockIdentifier() {
+            return ofBlockIdentifier(20, 146, 7);
+        }
+        
+        public static SelectionCellCreator<Identifier> ofBlockIdentifier(int maxItems) {
+            return ofBlockIdentifier(20, 146, maxItems);
+        }
+        
+        public static SelectionCellCreator<Identifier> ofBlockIdentifier(int cellHeight, int cellWidth, int maxItems) {
+            return new DefaultSelectionCellCreator<Identifier>() {
+                @Override
+                public DropdownBoxEntry.SelectionCellElement<Identifier> create(Identifier selection) {
+                    ItemStack s = new ItemStack(Registry.BLOCK.get(selection));
+                    return new DropdownBoxEntry.DefaultSelectionCellElement<Identifier>(selection, toStringFunction) {
+                        @Override
+                        public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                            rendering = true;
+                            this.x = x;
+                            this.y = y;
+                            this.width = width;
+                            this.height = height;
+                            boolean b = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
+                            if (b)
+                                fill(x + 1, y + 1, x + width - 1, y + height - 1, -15132391);
+                            MinecraftClient.getInstance().textRenderer.drawWithShadow(toStringFunction.apply(r), x + 6 + 18, y + 6, b ? 16777215 : 8947848);
+                            ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                            itemRenderer.renderGuiItemIcon(s, x + 4, y + 2);
+                        }
+                    };
+                }
+                
+                @Override
+                public int getCellHeight() {
+                    return cellHeight;
+                }
+                
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static SelectionCellCreator<Item> ofItemObject() {
+            return ofItemObject(20, 146, 7);
+        }
+        
+        public static SelectionCellCreator<Item> ofItemObject(int maxItems) {
+            return ofItemObject(20, 146, maxItems);
+        }
+        
+        public static SelectionCellCreator<Item> ofItemObject(int cellHeight, int cellWidth, int maxItems) {
+            return new DefaultSelectionCellCreator<Item>(i -> Registry.ITEM.getId(i).toString()) {
+                @Override
+                public DropdownBoxEntry.SelectionCellElement<Item> create(Item selection) {
+                    ItemStack s = new ItemStack(selection);
+                    return new DropdownBoxEntry.DefaultSelectionCellElement<Item>(selection, toStringFunction) {
+                        @Override
+                        public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                            rendering = true;
+                            this.x = x;
+                            this.y = y;
+                            this.width = width;
+                            this.height = height;
+                            boolean b = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
+                            if (b)
+                                fill(x + 1, y + 1, x + width - 1, y + height - 1, -15132391);
+                            MinecraftClient.getInstance().textRenderer.drawWithShadow(toStringFunction.apply(r), x + 6 + 18, y + 6, b ? 16777215 : 8947848);
+                            ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                            itemRenderer.renderGuiItemIcon(s, x + 4, y + 2);
+                        }
+                    };
+                }
+                
+                @Override
+                public int getCellHeight() {
+                    return cellHeight;
+                }
+                
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+        
+        public static SelectionCellCreator<Block> ofBlockObject() {
+            return ofBlockObject(20, 146, 7);
+        }
+        
+        public static SelectionCellCreator<Block> ofBlockObject(int maxItems) {
+            return ofBlockObject(20, 146, maxItems);
+        }
+        
+        public static SelectionCellCreator<Block> ofBlockObject(int cellHeight, int cellWidth, int maxItems) {
+            return new DefaultSelectionCellCreator<Block>(i -> Registry.BLOCK.getId(i).toString()) {
+                @Override
+                public DropdownBoxEntry.SelectionCellElement<Block> create(Block selection) {
+                    ItemStack s = new ItemStack(selection);
+                    return new DropdownBoxEntry.DefaultSelectionCellElement<Block>(selection, toStringFunction) {
+                        @Override
+                        public void render(int mouseX, int mouseY, int x, int y, int width, int height, float delta) {
+                            rendering = true;
+                            this.x = x;
+                            this.y = y;
+                            this.width = width;
+                            this.height = height;
+                            boolean b = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
+                            if (b)
+                                fill(x + 1, y + 1, x + width - 1, y + height - 1, -15132391);
+                            MinecraftClient.getInstance().textRenderer.drawWithShadow(toStringFunction.apply(r), x + 6 + 18, y + 6, b ? 16777215 : 8947848);
+                            ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+                            itemRenderer.renderGuiItemIcon(s, x + 4, y + 2);
+                        }
+                    };
+                }
+                
+                @Override
+                public int getCellHeight() {
+                    return cellHeight;
+                }
+                
+                @Override
+                public int getCellWidth() {
+                    return cellWidth;
+                }
+                
+                @Override
+                public int getDropBoxMaxHeight() {
+                    return getCellHeight() * maxItems;
+                }
+            };
+        }
+    }
+}

+ 14 - 6
src/main/java/me/shedaniel/clothconfig2/impl/builders/FieldBuilder.java

@@ -2,36 +2,44 @@ package me.shedaniel.clothconfig2.impl.builders;
 
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
 public abstract class FieldBuilder<T, A extends AbstractConfigListEntry> {
-    private final String fieldNameKey;
-    private final String resetButtonKey;
+    @Nonnull private final String fieldNameKey;
+    @Nonnull private final String resetButtonKey;
     protected boolean requireRestart = false;
-    protected Supplier<T> defaultValue = null;
-    protected Function<T, Optional<String>> errorSupplier;
+    @Nullable protected Supplier<T> defaultValue = null;
+    @Nullable protected Function<T, Optional<String>> errorSupplier;
     
     protected FieldBuilder(String resetButtonKey, String fieldNameKey) {
-        this.resetButtonKey = resetButtonKey;
-        this.fieldNameKey = fieldNameKey;
+        this.resetButtonKey = Objects.requireNonNull(resetButtonKey);
+        this.fieldNameKey = Objects.requireNonNull(fieldNameKey);
     }
     
+    @Nullable
     public final Supplier<T> getDefaultValue() {
         return defaultValue;
     }
     
+    @Deprecated
     public final AbstractConfigListEntry buildEntry() {
         return build();
     }
     
+    @Nonnull
     public abstract A build();
     
+    @Nonnull
     public final String getFieldNameKey() {
         return fieldNameKey;
     }
     
+    @Nonnull
     public final String getResetButtonKey() {
         return resetButtonKey;
     }

+ 2 - 1
src/main/java/me/shedaniel/clothconfig2/impl/builders/TextDescriptionBuilder.java

@@ -2,13 +2,14 @@ package me.shedaniel.clothconfig2.impl.builders;
 
 import me.shedaniel.clothconfig2.gui.entries.TextListEntry;
 
+import javax.annotation.Nullable;
 import java.util.Optional;
 import java.util.function.Supplier;
 
 public class TextDescriptionBuilder extends FieldBuilder<String, TextListEntry> {
     
     private int color = -1;
-    private Supplier<Optional<String[]>> tooltipSupplier = null;
+    @Nullable private Supplier<Optional<String[]>> tooltipSupplier = null;
     private String value;
     
     public TextDescriptionBuilder(String resetButtonKey, String fieldNameKey, String value) {

+ 4 - 1
src/main/resources/assets/cloth-config2/lang/en_us.json

@@ -21,5 +21,8 @@
   "text.cloth-config.restart_required": "Restart Required",
   "text.cloth-config.restart_required_sub": "One of your modified settings requires Minecraft to be restarted. Do you want to proceed?",
   "text.cloth-config.exit_minecraft": "Exit Minecraft",
-  "text.cloth-config.ignore_restart": "Ignore Restart"
+  "text.cloth-config.ignore_restart": "Ignore Restart",
+  "text.cloth-config.boolean.value.true": "§aYes",
+  "text.cloth-config.boolean.value.false": "§cNo",
+  "text.cloth-config.dropdown.value.unknown": "§cNo Suggestion"
 }