Переглянути джерело

4.4 remove smooth scroll and start working on abstracting config screen

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel 5 роки тому
батько
коміт
e4565d617e
33 змінених файлів з 932 додано та 794 видалено
  1. 1 1
      gradle.properties
  2. 9 214
      src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java
  3. 40 5
      src/main/java/me/shedaniel/clothconfig2/api/AbstractConfigEntry.java
  4. 1 1
      src/main/java/me/shedaniel/clothconfig2/api/AbstractConfigListEntry.java
  5. 6 1
      src/main/java/me/shedaniel/clothconfig2/api/ConfigBuilder.java
  6. 2 0
      src/main/java/me/shedaniel/clothconfig2/api/ConfigScreen.java
  7. 0 23
      src/main/java/me/shedaniel/clothconfig2/api/FakeModifierKeyCodeAdder.java
  8. 43 0
      src/main/java/me/shedaniel/clothconfig2/api/LazyResettable.java
  9. 5 13
      src/main/java/me/shedaniel/clothconfig2/api/QueuedTooltip.java
  10. 5 0
      src/main/java/me/shedaniel/clothconfig2/api/ReferenceBuildingConfigScreen.java
  11. 2 5
      src/main/java/me/shedaniel/clothconfig2/api/ScrollingContainer.java
  12. 23 0
      src/main/java/me/shedaniel/clothconfig2/api/Tooltip.java
  13. 292 4
      src/main/java/me/shedaniel/clothconfig2/gui/AbstractConfigScreen.java
  14. 3 2
      src/main/java/me/shedaniel/clothconfig2/gui/AbstractTabbedConfigScreen.java
  15. 65 337
      src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java
  16. 4 4
      src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigTabButton.java
  17. 0 2
      src/main/java/me/shedaniel/clothconfig2/gui/ClothRequiresRestartScreen.java
  18. 330 0
      src/main/java/me/shedaniel/clothconfig2/gui/GlobalizedClothConfigScreen.java
  19. 4 0
      src/main/java/me/shedaniel/clothconfig2/gui/entries/BaseListCell.java
  20. 10 2
      src/main/java/me/shedaniel/clothconfig2/gui/entries/BaseListEntry.java
  21. 3 3
      src/main/java/me/shedaniel/clothconfig2/gui/entries/KeyCodeEntry.java
  22. 4 2
      src/main/java/me/shedaniel/clothconfig2/gui/entries/MultiElementListEntry.java
  23. 21 2
      src/main/java/me/shedaniel/clothconfig2/gui/entries/NestedListListEntry.java
  24. 4 2
      src/main/java/me/shedaniel/clothconfig2/gui/entries/SubCategoryListEntry.java
  25. 2 2
      src/main/java/me/shedaniel/clothconfig2/gui/entries/TooltipListEntry.java
  26. 3 4
      src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicEntryListWidget.java
  27. 45 18
      src/main/java/me/shedaniel/clothconfig2/impl/ConfigBuilderImpl.java
  28. 4 6
      src/main/java/me/shedaniel/clothconfig2/impl/ConfigCategoryImpl.java
  29. 0 51
      src/main/java/me/shedaniel/clothconfig2/impl/FakeModifierKeyCodeAdderImpl.java
  30. 0 52
      src/main/java/me/shedaniel/clothconfig2/mixin/MixinControlsOptionsScreen.java
  31. 0 19
      src/main/java/me/shedaniel/clothconfig2/mixin/MixinGameOptions.java
  32. 0 18
      src/main/java/me/shedaniel/clothconfig2/mixin/MixinKeyBinding.java
  33. 1 1
      src/main/resources/mixin.cloth-config2.json

+ 1 - 1
gradle.properties

@@ -3,6 +3,6 @@ minecraft_version=20w18a
 yarn_mappings=20w18a+build.1
 loader_version=0.8.2+build.194
 fabric_version=0.7.1+build.331-1.16
-mod_version=4.3.2-unstable
+mod_version=4.4.0-unstable
 modmenu_version=1.11.2+build.6
 nec_version=1.2.3+1.15.1

+ 9 - 214
src/main/java/me/shedaniel/clothconfig2/ClothConfigInitializer.java

@@ -1,26 +1,19 @@
 package me.shedaniel.clothconfig2;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import me.shedaniel.clothconfig2.api.*;
-import me.shedaniel.clothconfig2.gui.entries.*;
+import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry;
+import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry;
 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 me.shedaniel.clothconfig2.impl.builders.SubCategoryBuilder;
 import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
-import net.fabricmc.fabric.impl.client.keybinding.KeyBindingRegistryImpl;
 import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.gui.Element;
-import net.minecraft.client.gui.widget.AbstractButtonWidget;
-import net.minecraft.client.gui.widget.AbstractPressableButtonWidget;
 import net.minecraft.client.util.InputUtil;
-import net.minecraft.client.util.Window;
-import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.item.Item;
 import net.minecraft.item.Items;
 import net.minecraft.text.LiteralText;
@@ -31,11 +24,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.ApiStatus;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileWriter;
 import java.lang.reflect.Method;
-import java.nio.file.Files;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -43,10 +32,6 @@ import java.util.stream.Collectors;
 public class ClothConfigInitializer implements ClientModInitializer {
     
     public static final Logger LOGGER = LogManager.getFormatterLogger("ClothConfig");
-    private static EasingMethod easingMethod = EasingMethodImpl.LINEAR;
-    private static long scrollDuration = 600;
-    private static double scrollStep = 19;
-    private static double bounceBackMultiplier = .24;
     
     @Deprecated
     @ApiStatus.ScheduledForRemoval
@@ -73,81 +58,23 @@ public class ClothConfigInitializer implements ClientModInitializer {
     }
     
     public static EasingMethod getEasingMethod() {
-        return easingMethod;
+        return EasingMethodImpl.NONE;
     }
     
     public static long getScrollDuration() {
-        return scrollDuration;
+        return 0;
     }
     
     public static double getScrollStep() {
-        return scrollStep;
+        return 16.0;
     }
     
     public static double getBounceBackMultiplier() {
-        return bounceBackMultiplier;
-    }
-    
-    private static void loadConfig() {
-        File file = new File(FabricLoader.getInstance().getConfigDirectory(), "cloth-config2/config.properties");
-        try {
-            file.getParentFile().mkdirs();
-            easingMethod = EasingMethodImpl.LINEAR;
-            scrollDuration = 600;
-            scrollStep = 19;
-            bounceBackMultiplier = .24;
-            if (!file.exists()) {
-                saveConfig();
-            }
-            Properties properties = new Properties();
-            properties.load(new FileInputStream(file));
-            String easing = properties.getProperty("easingMethod1", "LINEAR");
-            for (EasingMethod value : EasingMethods.getMethods()) {
-                if (value.toString().equalsIgnoreCase(easing)) {
-                    easingMethod = value;
-                    break;
-                }
-            }
-            scrollDuration = Long.parseLong(properties.getProperty("scrollDuration1", "600"));
-            scrollStep = Double.parseDouble(properties.getProperty("scrollStep1", "19"));
-            bounceBackMultiplier = Double.parseDouble(properties.getProperty("bounceBackMultiplier2", "0.24"));
-        } catch (Exception e) {
-            e.printStackTrace();
-            easingMethod = EasingMethodImpl.LINEAR;
-            scrollDuration = 600;
-            scrollStep = 19;
-            bounceBackMultiplier = .24;
-            try {
-                Files.deleteIfExists(file.toPath());
-            } catch (Exception ignored) {
-            }
-        }
-        saveConfig();
-    }
-    
-    private static void saveConfig() {
-        File file = new File(FabricLoader.getInstance().getConfigDirectory(), "cloth-config2/config.properties");
-        try {
-            FileWriter writer = new FileWriter(file, false);
-            Properties properties = new Properties();
-            properties.setProperty("easingMethod1", easingMethod.toString());
-            properties.setProperty("scrollDuration1", scrollDuration + "");
-            properties.setProperty("scrollStep1", scrollStep + "");
-            properties.setProperty("bounceBackMultiplier2", bounceBackMultiplier + "");
-            properties.store(writer, null);
-            writer.close();
-        } catch (Exception e) {
-            e.printStackTrace();
-            easingMethod = EasingMethodImpl.LINEAR;
-            scrollDuration = 600;
-            scrollStep = 19;
-            bounceBackMultiplier = .24;
-        }
+        return -10;
     }
     
     @Override
     public void onInitializeClient() {
-        loadConfig();
         if (FabricLoader.getInstance().isModLoaded("modmenu")) {
             try {
                 Class<?> clazz = Class.forName("io.github.prospector.modmenu.api.ModMenuApi");
@@ -163,126 +90,6 @@ public class ClothConfigInitializer implements ClientModInitializer {
                 ClothConfigInitializer.LOGGER.error("[ClothConfig] Failed to add test config override for ModMenu!", e);
             }
         }
-        if (FabricLoader.getInstance().isDevelopmentEnvironment()) {
-            try {
-                KeyBindingRegistryImpl.INSTANCE.addCategory("Cloth Test Keybinds");
-                FakeModifierKeyCodeAdder.INSTANCE.registerModifierKeyCode("Cloth Test Keybinds", "Keybind 1",
-                        ModifierKeyCode::unknown, ModifierKeyCode::unknown, System.out::println);
-            } catch (Throwable throwable) {
-                throwable.printStackTrace();
-            }
-        }
-    }
-    
-    @SuppressWarnings("deprecation")
-    public static ConfigBuilder getConfigBuilder() {
-        ConfigBuilder builder = ConfigBuilder.create().setParentScreen(MinecraftClient.getInstance().currentScreen).setTitle(new TranslatableText("title.cloth-config.config"));
-        builder.setDefaultBackgroundTexture(new Identifier("minecraft:textures/block/oak_planks.png"));
-        ConfigCategory scrolling = builder.getOrCreateCategory(new TranslatableText("category.cloth-config.scrolling"));
-        ConfigEntryBuilder entryBuilder = ConfigEntryBuilder.create();
-        DropdownBoxEntry<EasingMethod> easingMethodEntry = entryBuilder.startDropdownMenu(new LiteralText("Easing Method"), DropdownMenuBuilder.TopCellElementBuilder.of(easingMethod, str -> {
-            for (EasingMethod m : EasingMethods.getMethods())
-                if (m.toString().equals(str))
-                    return m;
-            return null;
-        })).setDefaultValue(EasingMethodImpl.LINEAR).setSaveConsumer(o -> easingMethod = o).setSelections(EasingMethods.getMethods()).build();
-        LongSliderEntry scrollDurationEntry = entryBuilder.startLongSlider(new TranslatableText("option.cloth-config.scrollDuration"), scrollDuration, 0, 5000).setTextGetter(integer -> new LiteralText(integer <= 0 ? "Value: Disabled" : (integer > 1500 ? String.format("Value: %.1fs", integer / 1000f) : "Value: " + integer + "ms"))).setDefaultValue(600).setSaveConsumer(i -> scrollDuration = i).build();
-        DoubleListEntry scrollStepEntry = entryBuilder.startDoubleField(new TranslatableText("option.cloth-config.scrollStep"), scrollStep).setDefaultValue(19).setSaveConsumer(i -> scrollStep = i).build();
-        LongSliderEntry bounceMultiplierEntry = entryBuilder.startLongSlider(new TranslatableText("option.cloth-config.bounceBackMultiplier"), (long) (bounceBackMultiplier * 1000), -10, 750).setTextGetter(integer -> new LiteralText(integer < 0 ? "Value: Disabled" : String.format("Value: %s", integer / 1000d))).setDefaultValue(240).setSaveConsumer(i -> bounceBackMultiplier = i / 1000d).build();
-        scrolling.addEntry(new TooltipListEntry<Object>(new TranslatableText("option.cloth-config.setDefaultSmoothScroll"), null) {
-            final int width = 220;
-            private final AbstractButtonWidget buttonWidget = new AbstractPressableButtonWidget(0, 0, 0, 20, getFieldName()) {
-                @Override
-                public void onPress() {
-                    easingMethodEntry.getSelectionElement().getTopRenderer().setValue(EasingMethodImpl.LINEAR);
-                    scrollDurationEntry.setValue(600);
-                    scrollStepEntry.setValue("19.0");
-                    bounceMultiplierEntry.setValue(240);
-                }
-            };
-            private final List<AbstractButtonWidget> children = ImmutableList.of(buttonWidget);
-            
-            @Override
-            public Object getValue() {
-                return null;
-            }
-            
-            @Override
-            public Optional<Object> getDefaultValue() {
-                return Optional.empty();
-            }
-            
-            @Override
-            public void save() {
-            }
-            
-            @Override
-            public List<? extends Element> children() {
-                return children;
-            }
-            
-            @Override
-            public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
-                super.render(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
-                Window window = MinecraftClient.getInstance().getWindow();
-                this.buttonWidget.active = this.isEditable();
-                this.buttonWidget.y = y;
-                this.buttonWidget.x = x + entryWidth / 2 - width / 2;
-                this.buttonWidget.setWidth(width);
-                this.buttonWidget.render(matrices, mouseX, mouseY, delta);
-            }
-        });
-        
-        scrolling.addEntry(new TooltipListEntry<Object>(new TranslatableText("option.cloth-config.disableSmoothScroll"), null) {
-            final int width = 220;
-            private final AbstractButtonWidget buttonWidget = new AbstractPressableButtonWidget(0, 0, 0, 20, getFieldName()) {
-                @Override
-                public void onPress() {
-                    easingMethodEntry.getSelectionElement().getTopRenderer().setValue(EasingMethodImpl.NONE);
-                    scrollDurationEntry.setValue(0);
-                    scrollStepEntry.setValue("16.0");
-                    bounceMultiplierEntry.setValue(-10);
-                }
-            };
-            private final List<AbstractButtonWidget> children = ImmutableList.of(buttonWidget);
-            
-            @Override
-            public Object getValue() {
-                return null;
-            }
-            
-            @Override
-            public Optional<Object> getDefaultValue() {
-                return Optional.empty();
-            }
-            
-            @Override
-            public void save() {
-            }
-            
-            @Override
-            public List<? extends Element> children() {
-                return children;
-            }
-            
-            @Override
-            public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
-                super.render(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
-                Window window = MinecraftClient.getInstance().getWindow();
-                this.buttonWidget.active = this.isEditable();
-                this.buttonWidget.y = y;
-                this.buttonWidget.x = x + entryWidth / 2 - width / 2;
-                this.buttonWidget.setWidth(width);
-                this.buttonWidget.render(matrices, mouseX, mouseY, delta);
-            }
-        });
-        scrolling.addEntry(easingMethodEntry);
-        scrolling.addEntry(scrollDurationEntry);
-        scrolling.addEntry(scrollStepEntry);
-        scrolling.addEntry(bounceMultiplierEntry);
-        builder.setSavingRunnable(ClothConfigInitializer::saveConfig);
-        builder.transparentBackground();
-        return builder;
     }
     
     public static ConfigBuilder getConfigBuilderWithDemo() {
@@ -322,7 +129,8 @@ public class ClothConfigInitializer implements ClientModInitializer {
             }
         }
         
-        ConfigBuilder builder = getConfigBuilder();
+        ConfigBuilder builder = ConfigBuilder.create().setParentScreen(MinecraftClient.getInstance().currentScreen).setTitle(new TranslatableText("title.cloth-config.config"));
+        builder.setDefaultBackgroundTexture(new Identifier("minecraft:textures/block/oak_planks.png"));
         ConfigEntryBuilder entryBuilder = builder.entryBuilder();
         ConfigCategory testing = builder.getOrCreateCategory(new TranslatableText("category.cloth-config.testing"));
         testing.addEntry(entryBuilder.startKeyCodeField(new LiteralText("Cool Key"), InputUtil.UNKNOWN_KEYCODE).setDefaultValue(InputUtil.UNKNOWN_KEYCODE).build());
@@ -383,20 +191,7 @@ public class ClothConfigInitializer implements ClientModInitializer {
                     }
                 }
         ));
+        builder.transparentBackground();
         return builder;
     }
-    
-    public static class Precision {
-        public static final float FLOAT_EPSILON = 1e-3f;
-        public static final double DOUBLE_EPSILON = 1e-7;
-        
-        public static boolean almostEquals(float value1, float value2, float acceptableDifference) {
-            return Math.abs(value1 - value2) <= acceptableDifference;
-        }
-        
-        public static boolean almostEquals(double value1, double value2, double acceptableDifference) {
-            return Math.abs(value1 - value2) <= acceptableDifference;
-        }
-    }
-    
 }

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

@@ -1,5 +1,6 @@
 package me.shedaniel.clothconfig2.api;
 
+import me.shedaniel.clothconfig2.gui.AbstractConfigScreen;
 import me.shedaniel.clothconfig2.gui.ClothConfigScreen;
 import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
 import net.fabricmc.api.EnvType;
@@ -8,14 +9,37 @@ import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.text.MutableText;
 import net.minecraft.text.Text;
 import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
+import java.util.List;
 import java.util.Optional;
 import java.util.function.Supplier;
 
 @Environment(EnvType.CLIENT)
 public abstract class AbstractConfigEntry<T> extends DynamicElementListWidget.ElementEntry<AbstractConfigEntry<T>> {
-    private ClothConfigScreen screen;
+    private AbstractConfigScreen screen;
     private Supplier<Optional<Text>> errorSupplier;
+    @Nullable
+    private List<AbstractConfigEntry<?>> referencableEntries = null;
+    
+    public final void setReferencableEntries(List<AbstractConfigEntry<?>> referencableEntries) {
+        this.referencableEntries = referencableEntries;
+    }
+    
+    public void requestReferenceRebuilding() {
+        AbstractConfigScreen configScreen = getConfigScreen();
+        if (configScreen instanceof ReferenceBuildingConfigScreen) {
+            ((ReferenceBuildingConfigScreen) configScreen).requestReferenceRebuilding();
+        }
+    }
+    
+    @Nullable
+    @ApiStatus.Internal
+    public final List<AbstractConfigEntry<?>> getReferencableEntries() {
+        return referencableEntries;
+    }
     
     public abstract boolean isRequiresRestart();
     
@@ -56,18 +80,29 @@ public abstract class AbstractConfigEntry<T> extends DynamicElementListWidget.El
     
     public abstract Optional<T> getDefaultValue();
     
-    public final ClothConfigScreen.ListWidget getParent() {
-        return screen.listWidget;
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
+    @Nullable
+    public final ClothConfigScreen getScreen() {
+        if (screen instanceof ClothConfigScreen)
+            return (ClothConfigScreen) screen;
+        return null;
     }
     
-    public final ClothConfigScreen getScreen() {
+    @Nullable
+    public final AbstractConfigScreen getConfigScreen() {
         return screen;
     }
     
+    public final void addTooltip(@NotNull Tooltip tooltip) {
+        screen.addTooltip(tooltip);
+    }
+    
     public void updateSelected(boolean isSelected) {}
     
     @Deprecated
-    public final void setScreen(ClothConfigScreen screen) {
+    @ApiStatus.Internal
+    public final void setScreen(AbstractConfigScreen screen) {
         this.screen = screen;
     }
     

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

@@ -26,7 +26,7 @@ public abstract class AbstractConfigListEntry<T> extends AbstractConfigEntry<T>
     }
     
     public boolean isEditable() {
-        return getScreen().isEditable() && editable;
+        return getConfigScreen().isEditable() && editable;
     }
     
     public void setEditable(boolean editable) {

+ 6 - 1
src/main/java/me/shedaniel/clothconfig2/api/ConfigBuilder.java

@@ -13,7 +13,6 @@ import java.util.function.Consumer;
 @Environment(EnvType.CLIENT)
 public interface ConfigBuilder {
     
-    @SuppressWarnings("deprecation")
     static ConfigBuilder create() {
         return new ConfigBuilderImpl();
     }
@@ -84,6 +83,12 @@ public interface ConfigBuilder {
         return setAlwaysShowTabs(true);
     }
     
+    /**
+     * @deprecated does not work
+     */
+    @Deprecated
+    void setGlobalized(boolean globalized);
+    
     boolean isAlwaysShowTabs();
     
     ConfigBuilder setAlwaysShowTabs(boolean alwaysShowTabs);

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

@@ -22,4 +22,6 @@ public interface ConfigScreen {
     void setEdited(boolean edited, boolean legacyRequiresRestart);
     
     void saveAll(boolean openOtherScreens);
+    
+    void addTooltip(Tooltip tooltip);
 }

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

@@ -1,23 +0,0 @@
-package me.shedaniel.clothconfig2.api;
-
-import me.shedaniel.clothconfig2.impl.FakeModifierKeyCodeAdderImpl;
-import net.fabricmc.api.EnvType;
-import net.fabricmc.api.Environment;
-import net.minecraft.client.options.KeyBinding;
-
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-@Environment(EnvType.CLIENT)
-public interface FakeModifierKeyCodeAdder {
-    FakeModifierKeyCodeAdder INSTANCE = new FakeModifierKeyCodeAdderImpl();
-    
-    void registerModifierKeyCode(String category, String translationKey, Supplier<ModifierKeyCode> keyCode, Supplier<ModifierKeyCode> defaultKeyCode, Consumer<ModifierKeyCode> onChanged);
-    
-    default void registerModifierKeyCode(String category, String translationKey, Supplier<ModifierKeyCode> keyCode, Consumer<ModifierKeyCode> onChanged) {
-        registerModifierKeyCode(category, translationKey, keyCode, keyCode, onChanged);
-    }
-    
-    List<KeyBinding> getFakeBindings();
-}

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

@@ -0,0 +1,43 @@
+package me.shedaniel.clothconfig2.api;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public final class LazyResettable<T> implements Supplier<T> {
+    private final Supplier<T> supplier;
+    private T value = null;
+    private boolean supplied = false;
+    
+    public LazyResettable(Supplier<T> supplier) {
+        this.supplier = supplier;
+    }
+    
+    @Override
+    public T get() {
+        if (!supplied) {
+            this.value = supplier.get();
+            this.supplied = true;
+        }
+        return value;
+    }
+    
+    public void reset() {
+        this.supplied = false;
+        this.value = null;
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        
+        LazyResettable<?> that = (LazyResettable<?>) o;
+        return Objects.equals(get(), that.get());
+    }
+    
+    @Override
+    public int hashCode() {
+        T value = get();
+        return value != null ? value.hashCode() : 0;
+    }
+}

+ 5 - 13
src/main/java/me/shedaniel/clothconfig2/api/QueuedTooltip.java

@@ -1,14 +1,13 @@
 package me.shedaniel.clothconfig2.api;
 
-import com.google.common.collect.Lists;
 import me.shedaniel.math.Point;
 import net.minecraft.text.Text;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-public class QueuedTooltip {
-    
+public class QueuedTooltip implements Tooltip {
     private Point location;
     private List<Text> text;
     
@@ -22,23 +21,16 @@ public class QueuedTooltip {
     }
     
     public static QueuedTooltip create(Point location, Text... text) {
-        return QueuedTooltip.create(location, Lists.newArrayList(text));
+        return QueuedTooltip.create(location, Arrays.asList(text));
     }
     
+    @Override
     public Point getPoint() {
         return location;
     }
     
-    public int getX() {
-        return location.x;
-    }
-    
-    public int getY() {
-        return location.y;
-    }
-    
+    @Override
     public List<Text> getText() {
         return text;
     }
-    
 }

+ 5 - 0
src/main/java/me/shedaniel/clothconfig2/api/ReferenceBuildingConfigScreen.java

@@ -0,0 +1,5 @@
+package me.shedaniel.clothconfig2.api;
+
+public interface ReferenceBuildingConfigScreen extends ConfigScreen {
+    void requestReferenceRebuilding();
+}

+ 2 - 5
src/main/java/me/shedaniel/clothconfig2/api/ScrollingContainer.java

@@ -34,7 +34,6 @@ import net.minecraft.client.render.BufferBuilder;
 import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexFormats;
 import net.minecraft.util.math.MathHelper;
-import org.jetbrains.annotations.ApiStatus;
 
 public abstract class ScrollingContainer {
     public double scrollAmount;
@@ -113,10 +112,7 @@ public abstract class ScrollingContainer {
             }
         } else
             target[0] = clampExtension(target[0], maxScroll, 0);
-        if (!ClothConfigInitializer.Precision.almostEquals(scroll, target[0], ClothConfigInitializer.Precision.FLOAT_EPSILON))
-            return ease(scroll, target[0], Math.min((System.currentTimeMillis() - start) / duration * delta * 3, 1), easingMethod);
-        else
-            return target[0];
+        return ease(scroll, target[0], Math.min((System.currentTimeMillis() - start) / duration * delta * 3, 1), easingMethod);
     }
     
     public static double ease(double start, double end, double amount, EasingMethod easingMethod) {
@@ -135,6 +131,7 @@ public abstract class ScrollingContainer {
         renderScrollBar(0, 1, 1);
     }
     
+    @SuppressWarnings("deprecation")
     public void renderScrollBar(int background, float alpha, float scrollBarAlphaOffset) {
         if (hasScrollBar()) {
             Rectangle bounds = getBounds();

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

@@ -0,0 +1,23 @@
+package me.shedaniel.clothconfig2.api;
+
+import me.shedaniel.math.Point;
+import net.minecraft.text.Text;
+
+import java.util.List;
+
+public interface Tooltip {
+    static Tooltip of(Point location, Text... text) {
+        return QueuedTooltip.create(location, text);
+    }
+    Point getPoint();
+    
+    default int getX() {
+        return getPoint().getX();
+    }
+    
+    default int getY() {
+        return getPoint().getY();
+    }
+    
+    List<Text> getText();
+}

+ 292 - 4
src/main/java/me/shedaniel/clothconfig2/gui/AbstractConfigScreen.java

@@ -1,21 +1,51 @@
 package me.shedaniel.clothconfig2.gui;
 
-import me.shedaniel.clothconfig2.api.AbstractConfigEntry;
-import me.shedaniel.clothconfig2.api.ConfigScreen;
+import com.google.common.collect.Lists;
+import com.mojang.blaze3d.systems.RenderSystem;
+import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
+import me.shedaniel.clothconfig2.api.*;
+import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
+import me.shedaniel.math.Rectangle;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.ConfirmScreen;
 import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.util.InputUtil;
+import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
 import net.minecraft.util.Identifier;
+import net.minecraft.util.Tickable;
+import net.minecraft.util.math.Matrix4f;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.List;
 import java.util.Map;
 
 public abstract class AbstractConfigScreen extends Screen implements ConfigScreen {
+    protected static final Identifier CONFIG_TEX = new Identifier("cloth-config2", "textures/gui/cloth_config.png");
     private boolean legacyEdited = false;
-    protected boolean legacyRequiresRestart = false;
     private final Identifier backgroundLocation;
+    protected boolean legacyRequiresRestart = false;
+    protected boolean confirmSave;
+    protected final Screen parent;
+    private boolean alwaysShowTabs = false;
+    private boolean transparentBackground = false;
+    @Nullable
+    private Text defaultFallbackCategory = null;
+    public int selectedCategoryIndex = 0;
+    private boolean editable = true;
+    private KeyCodeEntry focusedBinding;
+    private ModifierKeyCode startedKeyCode = null;
+    private final List<Tooltip> tooltips = Lists.newArrayList();
     
-    protected AbstractConfigScreen(Text title, Identifier backgroundLocation) {
+    protected AbstractConfigScreen(Screen parent, Text title, Identifier backgroundLocation) {
         super(title);
+        this.parent = parent;
         this.backgroundLocation = backgroundLocation;
     }
     
@@ -57,6 +87,7 @@ public abstract class AbstractConfigScreen extends Screen implements ConfigScree
      */
     @Override
     @Deprecated
+    @ApiStatus.ScheduledForRemoval
     public void setEdited(boolean edited) {
         this.legacyEdited = edited;
     }
@@ -66,9 +97,266 @@ public abstract class AbstractConfigScreen extends Screen implements ConfigScree
      */
     @Override
     @Deprecated
+    @ApiStatus.ScheduledForRemoval
     public void setEdited(boolean edited, boolean legacyRequiresRestart) {
         setEdited(edited);
         if (!this.legacyRequiresRestart && legacyRequiresRestart)
             this.legacyRequiresRestart = legacyRequiresRestart;
     }
+    
+    public boolean isShowingTabs() {
+        return isAlwaysShowTabs() || getCategorizedEntries().size() > 1;
+    }
+    
+    public boolean isAlwaysShowTabs() {
+        return alwaysShowTabs;
+    }
+    
+    @ApiStatus.Internal
+    public void setAlwaysShowTabs(boolean alwaysShowTabs) {
+        this.alwaysShowTabs = alwaysShowTabs;
+    }
+    
+    public boolean isTransparentBackground() {
+        return transparentBackground && MinecraftClient.getInstance().world != null;
+    }
+    
+    @ApiStatus.Internal
+    public void setTransparentBackground(boolean transparentBackground) {
+        this.transparentBackground = transparentBackground;
+    }
+    
+    public Text getFallbackCategory() {
+        if (defaultFallbackCategory != null)
+            return defaultFallbackCategory;
+        return getCategorizedEntries().keySet().iterator().next();
+    }
+    
+    @ApiStatus.Internal
+    public void setFallbackCategory(@Nullable Text defaultFallbackCategory) {
+        this.defaultFallbackCategory = defaultFallbackCategory;
+        List<Text> categories = Lists.newArrayList(getCategorizedEntries().keySet());
+        for (int i = 0; i < categories.size(); i++) {
+            Text category = categories.get(i);
+            if (category.equals(getFallbackCategory())) {
+                this.selectedCategoryIndex = i;
+                break;
+            }
+        }
+    }
+    
+    @Override
+    public void saveAll(boolean openOtherScreens) {
+        for (List<AbstractConfigEntry<?>> entries : Lists.newArrayList(getCategorizedEntries().values()))
+            for (AbstractConfigEntry<?> entry : entries)
+                entry.save();
+        save();
+        setEdited(false);
+        if (openOtherScreens) {
+            if (isRequiresRestart())
+                AbstractConfigScreen.this.client.openScreen(new ClothRequiresRestartScreen(parent));
+            else
+                AbstractConfigScreen.this.client.openScreen(parent);
+        }
+        this.legacyRequiresRestart = false;
+    }
+    
+    public void save() {
+    }
+    
+    public boolean isEditable() {
+        return editable;
+    }
+    
+    @ApiStatus.Internal
+    public void setEditable(boolean editable) {
+        this.editable = editable;
+    }
+    
+    @ApiStatus.Internal
+    public void setConfirmSave(boolean confirmSave) {
+        this.confirmSave = confirmSave;
+    }
+    
+    public KeyCodeEntry getFocusedBinding() {
+        return focusedBinding;
+    }
+    
+    @ApiStatus.Internal
+    public void setFocusedBinding(KeyCodeEntry focusedBinding) {
+        this.focusedBinding = focusedBinding;
+        if (focusedBinding != null) {
+            startedKeyCode = this.focusedBinding.getValue();
+            startedKeyCode.setKeyCodeAndModifier(InputUtil.UNKNOWN_KEYCODE, Modifier.none());
+        } else
+            startedKeyCode = null;
+    }
+    
+    @Override
+    public boolean mouseReleased(double double_1, double double_2, int int_1) {
+        if (this.focusedBinding != null && this.startedKeyCode != null && !this.startedKeyCode.isUnknown() && focusedBinding.isAllowMouse()) {
+            focusedBinding.setValue(startedKeyCode);
+            setFocusedBinding(null);
+            return true;
+        }
+        return super.mouseReleased(double_1, double_2, int_1);
+    }
+    
+    @Override
+    public boolean keyReleased(int int_1, int int_2, int int_3) {
+        if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowKey()) {
+            focusedBinding.setValue(startedKeyCode);
+            setFocusedBinding(null);
+            return true;
+        }
+        return super.keyReleased(int_1, int_2, int_3);
+    }
+    
+    @Override
+    public boolean mouseClicked(double double_1, double double_2, int int_1) {
+        if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowMouse()) {
+            if (startedKeyCode.isUnknown())
+                startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+            else if (focusedBinding.isAllowModifiers()) {
+                if (startedKeyCode.getType() == InputUtil.Type.KEYSYM) {
+                    int code = startedKeyCode.getKeyCode().getKeyCode();
+                    if (MinecraftClient.IS_SYSTEM_MAC ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
+                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+                        return true;
+                    } else if (code == 344 || code == 340) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
+                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+                        return true;
+                    } else if (code == 342 || code == 346) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
+                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
+                        return true;
+                    }
+                }
+            }
+            return true;
+        } else {
+            if (this.focusedBinding != null)
+                return true;
+            return super.mouseClicked(double_1, double_2, int_1);
+        }
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if (this.focusedBinding != null && (focusedBinding.isAllowKey() || int_1 == 256)) {
+            if (int_1 != 256) {
+                if (startedKeyCode.isUnknown())
+                    startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+                else if (focusedBinding.isAllowModifiers()) {
+                    if (startedKeyCode.getType() == InputUtil.Type.KEYSYM) {
+                        int code = startedKeyCode.getKeyCode().getKeyCode();
+                        if (MinecraftClient.IS_SYSTEM_MAC ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
+                            Modifier modifier = startedKeyCode.getModifier();
+                            startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
+                            startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+                            return true;
+                        } else if (code == 344 || code == 340) {
+                            Modifier modifier = startedKeyCode.getModifier();
+                            startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
+                            startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+                            return true;
+                        } else if (code == 342 || code == 346) {
+                            Modifier modifier = startedKeyCode.getModifier();
+                            startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
+                            startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
+                            return true;
+                        }
+                    }
+                    if (MinecraftClient.IS_SYSTEM_MAC ? (int_1 == 343 || int_1 == 347) : (int_1 == 341 || int_1 == 345)) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
+                        return true;
+                    } else if (int_1 == 344 || int_1 == 340) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
+                        return true;
+                    } else if (int_1 == 342 || int_1 == 346) {
+                        Modifier modifier = startedKeyCode.getModifier();
+                        startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
+                        return true;
+                    }
+                }
+            } else {
+                focusedBinding.setValue(ModifierKeyCode.unknown());
+                setFocusedBinding(null);
+            }
+            return true;
+        }
+        if (this.focusedBinding != null && int_1 != 256)
+            return true;
+        if (int_1 == 256 && this.shouldCloseOnEsc()) {
+            return quit();
+        }
+        return super.keyPressed(int_1, int_2, int_3);
+    }
+    
+    protected final boolean quit() {
+        if (confirmSave && isEdited())
+            client.openScreen(new ConfirmScreen(new QuitSaveConsumer(), new TranslatableText("text.cloth-config.quit_config"), new TranslatableText("text.cloth-config.quit_config_sure"), new TranslatableText("text.cloth-config.quit_discard"), new TranslatableText("gui.cancel")));
+        else
+            client.openScreen(parent);
+        return true;
+    }
+    
+    private class QuitSaveConsumer implements BooleanConsumer {
+        @Override
+        public void accept(boolean t) {
+            if (!t)
+                client.openScreen(AbstractConfigScreen.this);
+            else
+                client.openScreen(parent);
+        }
+    }
+    
+    @Override
+    public void tick() {
+        super.tick();
+        for (Element child : children())
+            if (child instanceof Tickable)
+                ((Tickable) child).tick();
+    }
+    
+    @Override
+    public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+        super.render(matrices, mouseX, mouseY, delta);
+        for (Tooltip tooltip : tooltips) {
+            renderTooltip(matrices, tooltip.getText(), tooltip.getX(), tooltip.getY());
+        }
+        this.tooltips.clear();
+    }
+    
+    @Override
+    public void addTooltip(Tooltip tooltip) {
+        this.tooltips.add(tooltip);
+    }
+    
+    protected void overlayBackground(MatrixStack matrices, Rectangle rect, int red, int green, int blue, int startAlpha, int endAlpha) {
+        overlayBackground(matrices.peek().getModel(), rect, red, green, blue, startAlpha, endAlpha);
+    }
+    
+    protected void overlayBackground(Matrix4f matrix, Rectangle rect, int red, int green, int blue, int startAlpha, int endAlpha) {
+        if (isTransparentBackground())
+            return;
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder buffer = tessellator.getBuffer();
+        client.getTextureManager().bindTexture(getBackgroundLocation());
+        RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        float f = 32.0F;
+        buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
+        buffer.vertex(matrix, rect.getMinX(), rect.getMaxY(), 0.0F).texture(rect.getMinX() / 32.0F, rect.getMaxY() / 32.0F).color(red, green, blue, endAlpha).next();
+        buffer.vertex(matrix, rect.getMaxX(), rect.getMaxY(), 0.0F).texture(rect.getMaxX() / 32.0F, rect.getMaxY() / 32.0F).color(red, green, blue, endAlpha).next();
+        buffer.vertex(matrix, rect.getMaxX(), rect.getMinY(), 0.0F).texture(rect.getMaxX() / 32.0F, rect.getMinY() / 32.0F).color(red, green, blue, startAlpha).next();
+        buffer.vertex(matrix, rect.getMinX(), rect.getMinY(), 0.0F).texture(rect.getMinX() / 32.0F, rect.getMinY() / 32.0F).color(red, green, blue, startAlpha).next();
+        tessellator.draw();
+    }
 }

+ 3 - 2
src/main/java/me/shedaniel/clothconfig2/gui/AbstractTabbedConfigScreen.java

@@ -2,6 +2,7 @@ package me.shedaniel.clothconfig2.gui;
 
 import com.google.common.collect.Maps;
 import me.shedaniel.clothconfig2.api.TabbedConfigScreen;
+import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
 
@@ -10,8 +11,8 @@ import java.util.Map;
 public abstract class AbstractTabbedConfigScreen extends AbstractConfigScreen implements TabbedConfigScreen {
     private final Map<Text, Identifier> categoryBackgroundLocation = Maps.newHashMap();
     
-    protected AbstractTabbedConfigScreen(Text title, Identifier backgroundLocation) {
-        super(title, backgroundLocation);
+    protected AbstractTabbedConfigScreen(Screen parent, Text title, Identifier backgroundLocation) {
+        super(parent, title, backgroundLocation);
     }
     
     @Override

+ 65 - 337
src/main/java/me/shedaniel/clothconfig2/gui/ClothConfigScreen.java

@@ -2,19 +2,13 @@ package me.shedaniel.clothconfig2.gui;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.util.concurrent.AtomicDouble;
 import com.mojang.blaze3d.systems.RenderSystem;
-import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
 import me.shedaniel.clothconfig2.api.*;
-import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
 import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
 import me.shedaniel.math.Rectangle;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.font.TextRenderer;
-import net.minecraft.client.gui.Element;
-import net.minecraft.client.gui.screen.ConfirmScreen;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.widget.AbstractButtonWidget;
 import net.minecraft.client.gui.widget.AbstractPressableButtonWidget;
@@ -23,17 +17,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.InputUtil;
 import net.minecraft.client.util.NarratorManager;
 import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.text.Text;
 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 net.minecraft.util.math.Matrix4f;
-import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.ApiStatus;
 
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -43,57 +34,54 @@ import java.util.stream.Collectors;
 @SuppressWarnings({"deprecation", "rawtypes", "DuplicatedCode"})
 @Environment(EnvType.CLIENT)
 public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
-    private static final Identifier CONFIG_TEX = new Identifier("cloth-config2", "textures/gui/cloth_config.png");
-    private final List<QueuedTooltip> queuedTooltips = Lists.newArrayList();
-    public int nextTabIndex = 0;
-    public int selectedTabIndex = 0;
-    public double tabsScrollVelocity = 0d;
-    public double tabsScrollProgress = 0d;
-    public ListWidget<AbstractConfigEntry<AbstractConfigEntry>> listWidget;
-    private KeyCodeEntry focusedBinding;
-    private final Screen parent;
+    private ScrollingContainer tabsScroller = new ScrollingContainer() {
+        @Override
+        public Rectangle getBounds() {
+            return new Rectangle(0, 0, 1, ClothConfigScreen.this.width - 40); // We don't need to handle dragging
+        }
+        
+        @Override
+        public int getMaxScrollHeight() {
+            return (int) ClothConfigScreen.this.getTabsMaximumScrolled();
+        }
+        
+        @Override
+        public void updatePosition(float delta) {
+            super.updatePosition(delta);
+            scrollAmount = clamp(scrollAmount, 0);
+        }
+    };
+    public ListWidget<AbstractConfigEntry<AbstractConfigEntry<?>>> listWidget;
     private final LinkedHashMap<Text, List<AbstractConfigEntry<?>>> categorizedEntries = Maps.newLinkedHashMap();
     private final List<Pair<Text, Integer>> tabs;
-    private final boolean confirmSave;
     private AbstractButtonWidget quitButton, saveButton, buttonLeftTab, buttonRightTab;
     private Rectangle tabsBounds, tabsLeftBounds, tabsRightBounds;
     private double tabsMaximumScrolled = -1d;
     private final List<ClothConfigTabButton> tabButtons = Lists.newArrayList();
-    private boolean transparentBackground = false;
-    private boolean editable = true;
-    @Nullable private Text defaultFallbackCategory = null;
-    private boolean alwaysShowTabs = false;
-    private ModifierKeyCode startedKeyCode = null;
     
     @Deprecated
-    public ClothConfigScreen(Screen parent, Text title, Map<Text, List<Pair<Text, Object>>> entriesMap, boolean confirmSave, Identifier backgroundLocation) {
-        super(title, backgroundLocation);
-        this.parent = parent;
-        entriesMap.forEach((categoryName, pairs) -> {
-            List<AbstractConfigEntry<?>> list = Lists.newArrayList();
-            for (Pair<Text, Object> pair : pairs) {
-                AbstractConfigListEntry<?> entry = (AbstractConfigListEntry<?>) pair.getRight();
+    public ClothConfigScreen(Screen parent, Text title, Map<Text, List<Object>> entriesMap, Identifier backgroundLocation) {
+        super(parent, title, backgroundLocation);
+        entriesMap.forEach((categoryName, list) -> {
+            List<AbstractConfigEntry<?>> entries = Lists.newArrayList();
+            for (Object object : list) {
+                AbstractConfigListEntry<?> entry;
+                if (object instanceof Pair<?, ?>) {
+                    entry = (AbstractConfigListEntry<?>) ((Pair<?, ?>) object).getRight();
+                } else {
+                    entry = (AbstractConfigListEntry<?>) object;
+                }
                 entry.setScreen(this);
-                list.add(entry);
+                entries.add(entry);
             }
-            categorizedEntries.put(categoryName, list);
+            categorizedEntries.put(categoryName, entries);
         });
-        TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
-        this.tabs = categorizedEntries.keySet().stream().map(s -> new Pair<>(s, textRenderer.getWidth(s) + 8)).collect(Collectors.toList());
-        for (int i = 0; i < tabs.size(); i++) {
-            Pair<Text, Integer> pair = tabs.get(i);
-            if (pair.getLeft().equals(getFallbackCategory())) {
-                this.nextTabIndex = i;
-                this.selectedTabIndex = i;
-                break;
-            }
-        }
-        this.confirmSave = confirmSave;
+        this.tabs = categorizedEntries.keySet().stream().map(s -> new Pair<>(s, MinecraftClient.getInstance().textRenderer.getWidth(s) + 8)).collect(Collectors.toList());
     }
     
     @Override
     public Text getSelectedCategory() {
-        return tabs.get(selectedTabIndex).getLeft();
+        return tabs.get(selectedCategoryIndex).getLeft();
     }
     
     @Override
@@ -101,53 +89,9 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
         return categorizedEntries;
     }
     
-    public boolean isShowingTabs() {
-        return isAlwaysShowTabs() || tabs.size() > 1;
-    }
-    
-    public boolean isAlwaysShowTabs() {
-        return alwaysShowTabs;
-    }
-    
-    @Deprecated
-    public void setAlwaysShowTabs(boolean alwaysShowTabs) {
-        this.alwaysShowTabs = alwaysShowTabs;
-    }
-    
-    public boolean isTransparentBackground() {
-        return transparentBackground && MinecraftClient.getInstance().world != null;
-    }
-    
-    @Deprecated
-    public void setTransparentBackground(boolean transparentBackground) {
-        this.transparentBackground = transparentBackground;
-    }
-    
-    public Text getFallbackCategory() {
-        if (defaultFallbackCategory != null)
-            return defaultFallbackCategory;
-        return tabs.get(0).getLeft();
-    }
-    
-    @Deprecated
-    public void setFallbackCategory(@Nullable Text defaultFallbackCategory) {
-        this.defaultFallbackCategory = defaultFallbackCategory;
-        for (int i = 0; i < tabs.size(); i++) {
-            Pair<Text, Integer> pair = tabs.get(i);
-            if (pair.getLeft().equals(getFallbackCategory())) {
-                this.nextTabIndex = i;
-                this.selectedTabIndex = i;
-                break;
-            }
-        }
-    }
-    
     @Override
     public void tick() {
         super.tick();
-        for (Element child : children())
-            if (child instanceof Tickable)
-                ((Tickable) child).tick();
         boolean edited = isEdited();
         quitButton.setMessage(edited ? new TranslatableText("text.cloth-config.cancel_discard") : new TranslatableText("gui.cancel"));
         saveButton.active = edited;
@@ -177,38 +121,21 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
     
     @Override
     public void saveAll(boolean openOtherScreens) {
-        for (List<AbstractConfigEntry<?>> entries : Lists.newArrayList(categorizedEntries.values()))
-            for (AbstractConfigEntry<?> entry : entries)
-                entry.save();
-        save();
-        setEdited(false);
-        if (openOtherScreens) {
-            if (isRequiresRestart())
-                ClothConfigScreen.this.client.openScreen(new ClothRequiresRestartScreen(parent));
-            else
-                ClothConfigScreen.this.client.openScreen(parent);
-        }
-        this.legacyRequiresRestart = false;
+        super.saveAll(openOtherScreens);
     }
     
     @Override
     protected void init() {
         super.init();
-        // Clear children
-        this.children.clear();
         this.tabButtons.clear();
         
-        selectedTabIndex = nextTabIndex;
-        children.add(listWidget = new ListWidget(client, width, height, isShowingTabs() ? 70 : 30, height - 32, getBackgroundLocation()));
-        if (categorizedEntries.size() > selectedTabIndex) {
-            listWidget.children().addAll((List) Lists.newArrayList(categorizedEntries.values()).get(selectedTabIndex));
+        children.add(listWidget = new ListWidget(this, client, width, height, isShowingTabs() ? 70 : 30, height - 32, getBackgroundLocation()));
+        if (categorizedEntries.size() > selectedCategoryIndex) {
+            listWidget.children().addAll((List) Lists.newArrayList(categorizedEntries.values()).get(selectedCategoryIndex));
         }
         int buttonWidths = Math.min(200, (width - 50 - 12) / 3);
         addButton(quitButton = new ButtonWidget(width / 2 - buttonWidths - 3, height - 26, buttonWidths, 20, isEdited() ? new TranslatableText("text.cloth-config.cancel_discard") : new TranslatableText("gui.cancel"), widget -> {
-            if (confirmSave && isEdited())
-                client.openScreen(new ConfirmScreen(new QuitSaveConsumer(), new TranslatableText("text.cloth-config.quit_config"), new TranslatableText("text.cloth-config.quit_config_sure"), new TranslatableText("text.cloth-config.quit_discard"), new TranslatableText("gui.cancel")));
-            else
-                client.openScreen(parent);
+            quit();
         }));
         addButton(saveButton = new AbstractPressableButtonWidget(width / 2 + 3, height - 26, buttonWidths, 20, NarratorManager.EMPTY) {
             @Override
@@ -241,9 +168,7 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
             children.add(buttonLeftTab = new AbstractPressableButtonWidget(4, 44, 12, 18, NarratorManager.EMPTY) {
                 @Override
                 public void onPress() {
-                    tabsScrollProgress = Integer.MIN_VALUE;
-                    tabsScrollVelocity = 0d;
-                    clampTabsScrolled();
+                    tabsScroller.scrollTo(0, false);
                 }
                 
                 @Override
@@ -266,9 +191,7 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
             children.add(buttonRightTab = new AbstractPressableButtonWidget(width - 16, 44, 12, 18, NarratorManager.EMPTY) {
                 @Override
                 public void onPress() {
-                    tabsScrollProgress = Integer.MAX_VALUE;
-                    tabsScrollVelocity = 0d;
-                    clampTabsScrolled();
+                    tabsScroller.scrollTo(tabsScroller.getMaxScroll(), false);
                 }
                 
                 @Override
@@ -290,10 +213,7 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
         if (tabsBounds.contains(double_1, double_2) && !tabsLeftBounds.contains(double_1, double_2) && !tabsRightBounds.contains(double_1, double_2) && double_3 != 0d) {
-            if (double_3 < 0)
-                tabsScrollVelocity += 16;
-            if (double_3 > 0)
-                tabsScrollVelocity -= 16;
+            tabsScroller.offset(-double_3 * 16, true);
             return true;
         }
         return super.mouseScrolled(double_1, double_2, double_3);
@@ -301,57 +221,28 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
     
     public double getTabsMaximumScrolled() {
         if (tabsMaximumScrolled == -1d) {
-            AtomicDouble d = new AtomicDouble();
-            tabs.forEach(pair -> d.addAndGet(pair.getRight() + 2));
-            tabsMaximumScrolled = d.get();
+            int[] i = {0};
+            for (Pair<Text, Integer> pair : tabs) i[0] += pair.getRight() + 2;
+            tabsMaximumScrolled = i[0];
         }
-        return tabsMaximumScrolled + 8;
+        return tabsMaximumScrolled + 6;
     }
     
     public void resetTabsMaximumScrolled() {
         tabsMaximumScrolled = -1d;
-        tabsScrollVelocity = 0f;
-    }
-    
-    public void clampTabsScrolled() {
-        int xx = 0;
-        for (ClothConfigTabButton tabButton : tabButtons)
-            xx += tabButton.getWidth() + 2;
-        if (xx > width - 40)
-            tabsScrollProgress = MathHelper.clamp(tabsScrollProgress, 0, getTabsMaximumScrolled() - width + 40);
-        else
-            tabsScrollProgress = 0d;
     }
     
     @Override
     public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
         if (isShowingTabs()) {
-            // TODO Rewrite code for smooth tabs
-            if (true) {
-                double change = tabsScrollVelocity * 0.2f;
-                if (change != 0) {
-                    if (change > 0 && change < .2)
-                        change = .2;
-                    else if (change < 0 && change > -.2)
-                        change = -.2;
-                    tabsScrollProgress += change;
-                    tabsScrollVelocity -= change;
-                    if (change > 0 == tabsScrollVelocity < 0)
-                        tabsScrollVelocity = 0f;
-                    clampTabsScrolled();
-                }
-            } else {
-                tabsScrollProgress += tabsScrollVelocity;
-                tabsScrollVelocity = 0d;
-                clampTabsScrolled();
-            }
-            int xx = 24 - (int) tabsScrollProgress;
+            tabsScroller.updatePosition(delta * 3);
+            int xx = 24 - (int) tabsScroller.scrollAmount;
             for (ClothConfigTabButton tabButton : tabButtons) {
                 tabButton.x = xx;
                 xx += tabButton.getWidth() + 2;
             }
-            buttonLeftTab.active = tabsScrollProgress > 0d;
-            buttonRightTab.active = tabsScrollProgress < getTabsMaximumScrolled() - width + 40;
+            buttonLeftTab.active = tabsScroller.scrollAmount > 0d;
+            buttonRightTab.active = tabsScroller.scrollAmount < getTabsMaximumScrolled() - width + 40;
         }
         if (isTransparentBackground()) {
             fillGradient(matrices, 0, 0, this.width, this.height, -1072689136, -804253680);
@@ -408,12 +299,12 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
             drawString(matrices, client.textRenderer, text, 18, 12, -1);
         }
         super.render(matrices, mouseX, mouseY, delta);
-        queuedTooltips.forEach(queuedTooltip -> renderTooltip(matrices, queuedTooltip.getText(), queuedTooltip.getX(), queuedTooltip.getY()));
-        queuedTooltips.clear();
     }
     
+    @ApiStatus.ScheduledForRemoval
+    @Deprecated
     public void queueTooltip(QueuedTooltip queuedTooltip) {
-        queuedTooltips.add(queuedTooltip);
+        super.addTooltip(queuedTooltip);
     }
     
     private void drawTabsShades(MatrixStack matrices, int lightColor, int darkColor) {
@@ -446,177 +337,23 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
         RenderSystem.disableBlend();
     }
     
-    protected void overlayBackground(MatrixStack matrices, Rectangle rect, int red, int green, int blue, int startAlpha, int endAlpha) {
-        overlayBackground(matrices.peek().getModel(), rect, red, green, blue, startAlpha, endAlpha);
-    }
-    
-    protected void overlayBackground(Matrix4f matrix, Rectangle rect, int red, int green, int blue, int startAlpha, int endAlpha) {
-        if (isTransparentBackground())
-            return;
-        Tessellator tessellator = Tessellator.getInstance();
-        BufferBuilder buffer = tessellator.getBuffer();
-        client.getTextureManager().bindTexture(getBackgroundLocation());
-        RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-        float f = 32.0F;
-        buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
-        buffer.vertex(matrix, rect.getMinX(), rect.getMaxY(), 0.0F).texture(rect.getMinX() / 32.0F, rect.getMaxY() / 32.0F).color(red, green, blue, endAlpha).next();
-        buffer.vertex(matrix, rect.getMaxX(), rect.getMaxY(), 0.0F).texture(rect.getMaxX() / 32.0F, rect.getMaxY() / 32.0F).color(red, green, blue, endAlpha).next();
-        buffer.vertex(matrix, rect.getMaxX(), rect.getMinY(), 0.0F).texture(rect.getMaxX() / 32.0F, rect.getMinY() / 32.0F).color(red, green, blue, startAlpha).next();
-        buffer.vertex(matrix, rect.getMinX(), rect.getMinY(), 0.0F).texture(rect.getMinX() / 32.0F, rect.getMinY() / 32.0F).color(red, green, blue, startAlpha).next();
-        tessellator.draw();
-    }
-    
-    public KeyCodeEntry getFocusedBinding() {
-        return focusedBinding;
-    }
-    
-    public void setFocusedBinding(KeyCodeEntry focusedBinding) {
-        this.focusedBinding = focusedBinding;
-        if (focusedBinding != null) {
-            startedKeyCode = this.focusedBinding.getValue();
-            startedKeyCode.setKeyCodeAndModifier(InputUtil.UNKNOWN_KEYCODE, Modifier.none());
-        } else
-            startedKeyCode = null;
-    }
-    
-    @Override
-    public boolean mouseReleased(double double_1, double double_2, int int_1) {
-        if (this.focusedBinding != null && this.startedKeyCode != null && !this.startedKeyCode.isUnknown() && focusedBinding.isAllowMouse()) {
-            focusedBinding.setValue(startedKeyCode);
-            setFocusedBinding(null);
-            return true;
-        }
-        return super.mouseReleased(double_1, double_2, int_1);
-    }
-    
-    @Override
-    public boolean keyReleased(int int_1, int int_2, int int_3) {
-        if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowKey()) {
-            focusedBinding.setValue(startedKeyCode);
-            setFocusedBinding(null);
-            return true;
-        }
-        return super.keyReleased(int_1, int_2, int_3);
-    }
-    
     @Override
-    public boolean mouseClicked(double double_1, double double_2, int int_1) {
-        if (this.focusedBinding != null && this.startedKeyCode != null && focusedBinding.isAllowMouse()) {
-            if (startedKeyCode.isUnknown())
-                startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
-            else if (focusedBinding.isAllowModifiers()) {
-                if (startedKeyCode.getType() == InputUtil.Type.KEYSYM) {
-                    int code = startedKeyCode.getKeyCode().getKeyCode();
-                    if (MinecraftClient.IS_SYSTEM_MAC ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
-                        Modifier modifier = startedKeyCode.getModifier();
-                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
-                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
-                        return true;
-                    } else if (code == 344 || code == 340) {
-                        Modifier modifier = startedKeyCode.getModifier();
-                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
-                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
-                        return true;
-                    } else if (code == 342 || code == 346) {
-                        Modifier modifier = startedKeyCode.getModifier();
-                        startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
-                        startedKeyCode.setKeyCode(InputUtil.Type.MOUSE.createFromCode(int_1));
-                        return true;
-                    }
-                }
-            }
-            return true;
-        } else {
-            if (this.focusedBinding != null)
-                return true;
-            return super.mouseClicked(double_1, double_2, int_1);
-        }
-    }
-    
-    @Override
-    public boolean keyPressed(int int_1, int int_2, int int_3) {
-        if (this.focusedBinding != null && (focusedBinding.isAllowKey() || int_1 == 256)) {
-            if (int_1 != 256) {
-                if (startedKeyCode.isUnknown())
-                    startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
-                else if (focusedBinding.isAllowModifiers()) {
-                    if (startedKeyCode.getType() == InputUtil.Type.KEYSYM) {
-                        int code = startedKeyCode.getKeyCode().getKeyCode();
-                        if (MinecraftClient.IS_SYSTEM_MAC ? (code == 343 || code == 347) : (code == 341 || code == 345)) {
-                            Modifier modifier = startedKeyCode.getModifier();
-                            startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
-                            startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
-                            return true;
-                        } else if (code == 344 || code == 340) {
-                            Modifier modifier = startedKeyCode.getModifier();
-                            startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
-                            startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
-                            return true;
-                        } else if (code == 342 || code == 346) {
-                            Modifier modifier = startedKeyCode.getModifier();
-                            startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
-                            startedKeyCode.setKeyCode(InputUtil.getKeyCode(int_1, int_2));
-                            return true;
-                        }
-                    }
-                    if (MinecraftClient.IS_SYSTEM_MAC ? (int_1 == 343 || int_1 == 347) : (int_1 == 341 || int_1 == 345)) {
-                        Modifier modifier = startedKeyCode.getModifier();
-                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), true, modifier.hasShift()));
-                        return true;
-                    } else if (int_1 == 344 || int_1 == 340) {
-                        Modifier modifier = startedKeyCode.getModifier();
-                        startedKeyCode.setModifier(Modifier.of(modifier.hasAlt(), modifier.hasControl(), true));
-                        return true;
-                    } else if (int_1 == 342 || int_1 == 346) {
-                        Modifier modifier = startedKeyCode.getModifier();
-                        startedKeyCode.setModifier(Modifier.of(true, modifier.hasControl(), modifier.hasShift()));
-                        return true;
-                    }
-                }
-            } else {
-                focusedBinding.setValue(ModifierKeyCode.unknown());
-                setFocusedBinding(null);
-            }
-            return true;
-        }
-        if (this.focusedBinding != null && int_1 != 256)
-            return true;
-        if (int_1 == 256 && this.shouldCloseOnEsc()) {
-            if (confirmSave && isEdited())
-                client.openScreen(new ConfirmScreen(new QuitSaveConsumer(), new TranslatableText("text.cloth-config.quit_config"), new TranslatableText("text.cloth-config.quit_config_sure"), new TranslatableText("text.cloth-config.quit_discard"), new TranslatableText("gui.cancel")));
-            else
-                client.openScreen(parent);
-            return true;
-        }
-        return super.keyPressed(int_1, int_2, int_3);
-    }
-    
     public void save() {
+        super.save();
     }
     
+    @Override
     public boolean isEditable() {
-        return editable;
-    }
-    
-    @Deprecated
-    public void setEditable(boolean editable) {
-        this.editable = editable;
-    }
-    
-    private class QuitSaveConsumer implements BooleanConsumer {
-        @Override
-        public void accept(boolean t) {
-            if (!t)
-                client.openScreen(ClothConfigScreen.this);
-            else
-                client.openScreen(parent);
-        }
+        return super.isEditable();
     }
     
-    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) {
+    public static class ListWidget<R extends DynamicElementListWidget.ElementEntry<R>> extends DynamicElementListWidget<R> {
+        private AbstractConfigScreen screen;
+        
+        public ListWidget(AbstractConfigScreen screen, MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
             super(client, width, height, top, bottom, backgroundLocation);
-            visible = false;
+            setRenderSelection(false);
+            this.screen = screen;
         }
         
         @Override
@@ -624,17 +361,9 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
             return width - 80;
         }
         
-        public ClothConfigScreen getScreen() {
-            return ClothConfigScreen.this;
-        }
-        
         @Override
         protected int getScrollbarPosition() {
-            return width - 36;
-        }
-        
-        protected final void clearStuff() {
-            this.clearItems();
+            return left + width - 36;
         }
         
         @Override
@@ -668,7 +397,7 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
         
         @Override
         protected void renderBackBackground(MatrixStack matrices, BufferBuilder buffer, Tessellator tessellator) {
-            if (!isTransparentBackground())
+            if (!screen.isTransparentBackground())
                 super.renderBackBackground(matrices, buffer, tessellator);
             else {
                 fillGradient(matrices, left, top, right, bottom, 0x68000000, 0x68000000);
@@ -677,9 +406,8 @@ public abstract class ClothConfigScreen extends AbstractTabbedConfigScreen {
         
         @Override
         protected void renderHoleBackground(MatrixStack matrices, int int_1, int int_2, int int_3, int int_4) {
-            if (!isTransparentBackground())
+            if (!screen.isTransparentBackground())
                 super.renderHoleBackground(matrices, int_1, int_2, int_3, int_4);
         }
     }
-    
 }

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

@@ -2,6 +2,7 @@ package me.shedaniel.clothconfig2.gui;
 
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.widget.AbstractPressableButtonWidget;
 import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.text.Text;
@@ -21,14 +22,13 @@ public class ClothConfigTabButton extends AbstractPressableButtonWidget {
     @Override
     public void onPress() {
         if (index != -1)
-            screen.nextTabIndex = index;
-        screen.tabsScrollVelocity = 0d;
-        screen.init();
+            screen.selectedCategoryIndex = index;
+        screen.init(MinecraftClient.getInstance(), screen.width, screen.height);
     }
     
     @Override
     public void render(MatrixStack matrices, int int_1, int int_2, float float_1) {
-        active = index != screen.selectedTabIndex;
+        active = index != screen.selectedCategoryIndex;
         super.render(matrices, int_1, int_2, float_1);
     }
     

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

@@ -9,7 +9,6 @@ import net.minecraft.text.TranslatableText;
 
 @Environment(EnvType.CLIENT)
 public class ClothRequiresRestartScreen extends ConfirmScreen {
-    
     public ClothRequiresRestartScreen(Screen parent) {
         super(t -> {
             if (t)
@@ -18,5 +17,4 @@ public class ClothRequiresRestartScreen extends ConfirmScreen {
                 MinecraftClient.getInstance().openScreen(parent);
         }, new TranslatableText("text.cloth-config.restart_required"), new TranslatableText("text.cloth-config.restart_required_sub"), new TranslatableText("text.cloth-config.exit_minecraft"), new TranslatableText("text.cloth-config.ignore_restart"));
     }
-    
 }

+ 330 - 0
src/main/java/me/shedaniel/clothconfig2/gui/GlobalizedClothConfigScreen.java

@@ -0,0 +1,330 @@
+package me.shedaniel.clothconfig2.gui;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.clothconfig2.api.*;
+import me.shedaniel.math.Rectangle;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.AbstractPressableButtonWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.util.NarratorManager;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.LiteralText;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+import net.minecraft.util.math.Matrix4f;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.*;
+
+public class GlobalizedClothConfigScreen extends AbstractConfigScreen implements ReferenceBuildingConfigScreen {
+    public ClothConfigScreen.ListWidget<AbstractConfigEntry<AbstractConfigEntry<?>>> listWidget;
+    private final LinkedHashMap<Text, List<AbstractConfigEntry<?>>> categorizedEntries = Maps.newLinkedHashMap();
+    private final ScrollingContainer sideSlider = new ScrollingContainer() {
+        private Rectangle empty = new Rectangle();
+        
+        @Override
+        public Rectangle getBounds() {
+            return empty;
+        }
+        
+        @Override
+        public int getMaxScrollHeight() {
+            return GlobalizedClothConfigScreen.this.sideExpandLimit.get();
+        }
+    };
+    private final List<Reference> references = Lists.newArrayList();
+    private final LazyResettable<Integer> sideExpandLimit = new LazyResettable<>(() -> {
+        int max = 0;
+        for (Reference reference : references) {
+            Text category = reference.getText();
+            int width = textRenderer.getWidth(new LiteralText(StringUtils.repeat("    ", reference.getIndent()) + "- ").append(category));
+            if (width > max) max = width;
+        }
+        return max + 8;
+    });
+    private boolean requestingReferenceRebuilding = false;
+    
+    @Deprecated
+    protected GlobalizedClothConfigScreen(Screen parent, Text title, Map<Text, List<Object>> entriesMap, Identifier backgroundLocation) {
+        super(parent, title, backgroundLocation);
+        entriesMap.forEach((categoryName, list) -> {
+            List<AbstractConfigEntry<?>> entries = Lists.newArrayList();
+            for (Object object : list) {
+                AbstractConfigListEntry<?> entry;
+                if (object instanceof Pair<?, ?>) {
+                    entry = (AbstractConfigListEntry<?>) ((Pair<?, ?>) object).getRight();
+                } else {
+                    entry = (AbstractConfigListEntry<?>) object;
+                }
+                entry.setScreen(this);
+                entries.add(entry);
+            }
+            categorizedEntries.put(categoryName, entries);
+        });
+    }
+    
+    @Override
+    public void requestReferenceRebuilding() {
+        this.requestingReferenceRebuilding = true;
+    }
+    
+    @Override
+    public Map<Text, List<AbstractConfigEntry<?>>> getCategorizedEntries() {
+        return this.categorizedEntries;
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Override
+    protected void init() {
+        super.init();
+        this.sideExpandLimit.reset();
+        this.references.clear();
+        buildReferences();
+        this.children.add(listWidget = new ClothConfigScreen.ListWidget<>(this, client, width - 14, height, 30, height - 32, getBackgroundLocation()));
+        this.listWidget.setLeftPos(14);
+        this.sideSlider.scrollTo(14, false);
+        this.categorizedEntries.forEach((category, entries) -> {
+            if (!listWidget.children().isEmpty())
+                this.listWidget.children().add((AbstractConfigEntry) new EmptyEntry(5));
+            this.listWidget.children().add((AbstractConfigEntry) new EmptyEntry(4));
+            this.listWidget.children().add((AbstractConfigEntry) new TextEntry(category.shallowCopy().formatted(Formatting.BOLD)));
+            this.listWidget.children().add((AbstractConfigEntry) new EmptyEntry(2));
+            this.listWidget.children().addAll((List) entries);
+        });
+        int buttonWidths = Math.min(200, (width - 50 - 12) / 3);
+        addButton(new ButtonWidget(width / 2 - buttonWidths - 3, height - 26, buttonWidths, 20, isEdited() ? new TranslatableText("text.cloth-config.cancel_discard") : new TranslatableText("gui.cancel"), widget -> {
+            quit();
+        }));
+        addButton(new AbstractPressableButtonWidget(width / 2 + 3, height - 26, buttonWidths, 20, NarratorManager.EMPTY) {
+            @Override
+            public void onPress() {
+                saveAll(true);
+            }
+            
+            @Override
+            public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+                boolean hasErrors = false;
+                label:
+                for (List<AbstractConfigEntry<?>> entries : categorizedEntries.values()) {
+                    for (AbstractConfigEntry<?> entry : entries) {
+                        if (entry.getConfigError().isPresent()) {
+                            hasErrors = true;
+                            break label;
+                        }
+                    }
+                }
+                active = isEdited() && !hasErrors;
+                setMessage(hasErrors ? new TranslatableText("text.cloth-config.error_cannot_save") : new TranslatableText("text.cloth-config.save_and_done"));
+                super.render(matrices, mouseX, mouseY, delta);
+            }
+        });
+    }
+    
+    private void buildReferences() {
+        categorizedEntries.forEach((categoryText, entries) -> {
+            this.references.add(new CategoryReference(categoryText));
+            for (AbstractConfigEntry<?> entry : entries) buildReferenceFor(entry, 0);
+        });
+    }
+    
+    private void buildReferenceFor(AbstractConfigEntry<?> entry, int layer) {
+        List<AbstractConfigEntry<?>> referencableEntries = entry.getReferencableEntries();
+        if (referencableEntries != null) {
+            this.references.add(new ConfigEntryReference(entry, layer));
+            for (AbstractConfigEntry<?> referencableEntry : referencableEntries) {
+                buildReferenceFor(referencableEntry, layer + 1);
+            }
+        }
+    }
+    
+    @Override
+    public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+        if (requestingReferenceRebuilding) {
+            this.references.clear();
+            buildReferences();
+            requestingReferenceRebuilding = false;
+        }
+        if (isTransparentBackground()) {
+            fillGradient(matrices, 0, 0, this.width, this.height, -1072689136, -804253680);
+        } else {
+            renderDirtBackground(0);
+        }
+        listWidget.render(matrices, mouseX, mouseY, delta);
+        ScissorsHandler.INSTANCE.scissor(new Rectangle(listWidget.left, listWidget.top, listWidget.width, listWidget.bottom - listWidget.top));
+        for (AbstractConfigEntry<?> child : listWidget.children())
+            child.lateRender(matrices, mouseX, mouseY, delta);
+        ScissorsHandler.INSTANCE.removeLastScissor();
+        method_27534(matrices, client.textRenderer, title, width / 2, 12, -1);
+        super.render(matrices, mouseX, mouseY, delta);
+        if (isTransparentBackground()) {
+//            fillGradient(matrices, 0, 0, (int) sideSlider.scrollAmount, height, -1072689136, -804253680);
+        } else {
+//            overlayBackground(matrices, new Rectangle(0, 0, (int) sideSlider.scrollAmount, height), 64, 64, 64, 255, 255);
+        }
+        sideSlider.updatePosition(delta);
+        {
+            Matrix4f matrix = matrices.peek().getModel();
+            RenderSystem.enableBlend();
+            RenderSystem.blendFuncSeparate(770, 771, 0, 1);
+            RenderSystem.disableAlphaTest();
+            RenderSystem.shadeModel(7425);
+            RenderSystem.disableTexture();
+            Tessellator tessellator = Tessellator.getInstance();
+            BufferBuilder buffer = tessellator.getBuffer();
+            buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
+            int shadeColor = isTransparentBackground() ? 120 : 255;
+            buffer.vertex(matrix, (int) sideSlider.scrollAmount + 4, height, 100.0F).texture(0, 1f).color(0, 0, 0, shadeColor).next();
+            buffer.vertex(matrix, (int) sideSlider.scrollAmount, height, 100.0F).texture(1f, 1f).color(0, 0, 0, shadeColor).next();
+            buffer.vertex(matrix, (int) sideSlider.scrollAmount, 0, 100.0F).texture(1f, 0).color(0, 0, 0, shadeColor).next();
+            buffer.vertex(matrix, (int) sideSlider.scrollAmount + 4, 0, 100.0F).texture(0, 0).color(0, 0, 0, shadeColor).next();
+            tessellator.draw();
+            RenderSystem.enableTexture();
+            RenderSystem.shadeModel(7424);
+            RenderSystem.enableAlphaTest();
+            RenderSystem.disableBlend();
+        }
+    }
+    
+    private static class EmptyEntry extends AbstractConfigListEntry<Object> {
+        private final int height;
+        
+        public EmptyEntry(int height) {
+            super(new LiteralText(UUID.randomUUID().toString()), false);
+            this.height = height;
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return height;
+        }
+        
+        @Override
+        public Object getValue() {
+            return null;
+        }
+        
+        @Override
+        public Optional<Object> getDefaultValue() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public void save() {}
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {}
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.emptyList();
+        }
+    }
+    
+    private static class TextEntry extends AbstractConfigListEntry<Object> {
+        private final Text text;
+        
+        public TextEntry(Text text) {
+            super(new LiteralText(UUID.randomUUID().toString()), false);
+            this.text = text;
+        }
+        
+        @Override
+        public int getItemHeight() {
+            List<Text> strings = MinecraftClient.getInstance().textRenderer.wrapLines(text, getParent().getItemWidth());
+            if (strings.isEmpty())
+                return 0;
+            return 4 + strings.size() * 10;
+        }
+        
+        @Override
+        public Object getValue() {
+            return null;
+        }
+        
+        @Override
+        public Optional<Object> getDefaultValue() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public void save() {}
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {
+            int yy = y + 2;
+            List<Text> texts = MinecraftClient.getInstance().textRenderer.wrapLines(this.text, getParent().getItemWidth());
+            for (Text text : texts) {
+                MinecraftClient.getInstance().textRenderer.drawWithShadow(matrices, text, x - 4 + entryWidth / 2 - MinecraftClient.getInstance().textRenderer.getWidth(text) / 2, yy, -1);
+                yy += 10;
+            }
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.emptyList();
+        }
+    }
+    
+    private interface Reference {
+        default int getIndent() {
+            return 0;
+        }
+        
+        Text getText();
+        
+        float getScale();
+    }
+    
+    private static class CategoryReference implements Reference {
+        private Text category;
+        
+        public CategoryReference(Text category) {
+            this.category = category.shallowCopy().formatted(Formatting.BOLD);
+        }
+        
+        @Override
+        public Text getText() {
+            return category;
+        }
+        
+        @Override
+        public float getScale() {
+            return 1.0F;
+        }
+    }
+    
+    private static class ConfigEntryReference implements Reference {
+        private AbstractConfigEntry<?> entry;
+        private int layer;
+        
+        public ConfigEntryReference(AbstractConfigEntry<?> entry, int layer) {
+            this.entry = entry;
+            this.layer = layer;
+        }
+        
+        @Override
+        public int getIndent() {
+            return layer;
+        }
+        
+        @Override
+        public Text getText() {
+            return entry.getFieldName();
+        }
+        
+        @Override
+        public float getScale() {
+            return 0.5F;
+        }
+    }
+}

+ 4 - 0
src/main/java/me/shedaniel/clothconfig2/gui/entries/BaseListCell.java

@@ -42,4 +42,8 @@ public abstract class BaseListCell extends AbstractParentElement {
     public boolean isEdited() {
         return getConfigError().isPresent();
     }
+    
+    public void onAdd() {}
+    
+    public void onDelete() {}
 }

+ 10 - 2
src/main/java/me/shedaniel/clothconfig2/gui/entries/BaseListEntry.java

@@ -2,7 +2,7 @@ package me.shedaniel.clothconfig2.gui.entries;
 
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.systems.RenderSystem;
-import me.shedaniel.clothconfig2.api.QueuedTooltip;
+import me.shedaniel.clothconfig2.api.Tooltip;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import net.fabricmc.api.EnvType;
@@ -77,8 +77,14 @@ public abstract class BaseListEntry<T, C extends BaseListCell, SELF extends Base
         this.widgets = Lists.newArrayList(labelWidget);
         this.resetWidget = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(resetButtonKey) + 6, 20, resetButtonKey, widget -> {
             widgets.removeAll(cells);
+            for (C cell : cells) {
+                cell.onDelete();
+            }
             cells.clear();
             defaultValue.get().stream().map(this::getFromValue).forEach(cells::add);
+            for (C cell : cells) {
+                cell.onAdd();
+            }
             widgets.addAll(cells);
         });
         this.widgets.add(resetWidget);
@@ -223,7 +229,7 @@ public abstract class BaseListEntry<T, C extends BaseListCell, SELF extends Base
         if (isMouseInside(mouseX, mouseY, x, y, entryWidth, entryHeight)) {
             Optional<Text[]> tooltip = getTooltip(mouseX, mouseY);
             if (tooltip.isPresent() && tooltip.get().length > 0)
-                getScreen().queueTooltip(QueuedTooltip.create(new Point(mouseX, mouseY), tooltip.get()));
+                addTooltip(Tooltip.of(new Point(mouseX, mouseY), tooltip.get()));
         }
         MinecraftClient.getInstance().getTextureManager().bindTexture(CONFIG_TEX);
         DiffuseLighting.disable();
@@ -277,11 +283,13 @@ public abstract class BaseListEntry<T, C extends BaseListCell, SELF extends Base
                     cells.add(cell = createNewInstance.apply(BaseListEntry.this.self()));
                     widgets.add(cell);
                 }
+                cell.onAdd();
                 MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
                 return true;
             } else if (isDeleteButtonEnabled() && isInsideDelete(double_1, double_2)) {
                 Element focused = getFocused();
                 if (expanded && focused instanceof BaseListCell) {
+                    ((BaseListCell) focused).onDelete();
                     //noinspection SuspiciousMethodCalls
                     cells.remove(focused);
                     widgets.remove(focused);

+ 3 - 3
src/main/java/me/shedaniel/clothconfig2/gui/entries/KeyCodeEntry.java

@@ -40,11 +40,11 @@ public class KeyCodeEntry extends TooltipListEntry<ModifierKeyCode> {
         this.value = value.copy();
         this.original = value.copy();
         this.buttonWidget = new ButtonWidget(0, 0, 150, 20, NarratorManager.EMPTY, widget -> {
-            getScreen().setFocusedBinding(this);
+            getConfigScreen().setFocusedBinding(this);
         });
         this.resetButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(resetButtonKey) + 6, 20, resetButtonKey, widget -> {
             KeyCodeEntry.this.value = getDefaultValue().orElse(null).copy();
-            getScreen().setFocusedBinding(null);
+            getConfigScreen().setFocusedBinding(null);
         });
         this.saveConsumer = saveConsumer;
         this.widgets = Lists.newArrayList(buttonWidget, resetButton);
@@ -112,7 +112,7 @@ public class KeyCodeEntry extends TooltipListEntry<ModifierKeyCode> {
         this.buttonWidget.active = isEditable();
         this.buttonWidget.y = y;
         this.buttonWidget.setMessage(getLocalizedName());
-        if (getScreen().getFocusedBinding() == this)
+        if (getConfigScreen().getFocusedBinding() == this)
             this.buttonWidget.setMessage(new LiteralText("> ").formatted(Formatting.WHITE).append(this.buttonWidget.getMessage().copy().formatted(Formatting.YELLOW)).append(new LiteralText(" <").formatted(Formatting.WHITE)));
         Text displayedFieldName = getDisplayedFieldName();
         if (MinecraftClient.getInstance().textRenderer.isRightToLeft()) {

+ 4 - 2
src/main/java/me/shedaniel/clothconfig2/gui/entries/MultiElementListEntry.java

@@ -3,6 +3,7 @@ package me.shedaniel.clothconfig2.gui.entries;
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
 import me.shedaniel.math.Rectangle;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
@@ -43,6 +44,7 @@ public class MultiElementListEntry<T> extends TooltipListEntry<T> {
         this.widget = new MultiElementListEntry<T>.CategoryLabelWidget();
         this.children = Lists.newArrayList(widget);
         this.children.addAll(entries);
+        this.setReferencableEntries((List) entries);
     }
     
     @Override
@@ -100,8 +102,8 @@ public class MultiElementListEntry<T> extends TooltipListEntry<T> {
         drawTexture(matrices, x - 15, y + 4, 24, (widget.rectangle.contains(mouseX, mouseY) ? 18 : 0) + (expanded ? 9 : 0), 9, 9);
         MinecraftClient.getInstance().textRenderer.drawWithShadow(matrices, getDisplayedFieldName(), x, y + 5, widget.rectangle.contains(mouseX, mouseY) ? 0xffe6fe16 : -1);
         for (AbstractConfigListEntry<?> entry : entries) {
-            entry.setParent(getParent());
-            entry.setScreen(getScreen());
+            entry.setParent((DynamicEntryListWidget) getParent());
+            entry.setScreen(getConfigScreen());
         }
         if (expanded) {
             int yy = y + 24;

+ 21 - 2
src/main/java/me/shedaniel/clothconfig2/gui/entries/NestedListListEntry.java

@@ -1,7 +1,10 @@
 package me.shedaniel.clothconfig2.gui.entries;
 
+import com.google.common.collect.Lists;
+import me.shedaniel.clothconfig2.api.AbstractConfigEntry;
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry.NestedListCell;
+import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.gui.Element;
@@ -23,11 +26,13 @@ import java.util.function.Supplier;
  */
 @Environment(EnvType.CLIENT)
 public final class NestedListListEntry<T, INNER extends AbstractConfigListEntry<T>> extends AbstractListListEntry<T, NestedListCell<T, INNER>, NestedListListEntry<T, INNER>> {
+    private final List<AbstractConfigEntry<?>> referencableEntries = Lists.newArrayList();
     
     @ApiStatus.Internal
     @Deprecated
     public NestedListListEntry(Text fieldName, List<T> value, boolean defaultExpanded, Supplier<Optional<Text[]>> tooltipSupplier, Consumer<List<T>> saveConsumer, Supplier<List<T>> defaultValue, Text resetButtonKey, boolean deleteButtonEnabled, boolean insertInFront, BiFunction<T, NestedListListEntry<T, INNER>, INNER> createNewCell) {
         super(fieldName, value, defaultExpanded, null, null, defaultValue, resetButtonKey, false, deleteButtonEnabled, insertInFront, (t, nestedListListEntry) -> new NestedListCell<>(t, nestedListListEntry, createNewCell.apply(t, nestedListListEntry)));
+        setReferencableEntries(referencableEntries);
     }
     
     @Override
@@ -65,7 +70,8 @@ public final class NestedListListEntry<T, INNER extends AbstractConfigListEntry<
         
         @Override
         public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
-            nestedEntry.setScreen(listListEntry.getScreen());
+            nestedEntry.setParent((DynamicEntryListWidget) listListEntry.getParent());
+            nestedEntry.setScreen(listListEntry.getConfigScreen());
             nestedEntry.render(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
         }
         
@@ -88,6 +94,19 @@ public final class NestedListListEntry<T, INNER extends AbstractConfigListEntry<
         public boolean isEdited() {
             return super.isEdited() || nestedEntry.isEdited();
         }
+        
+        @Override
+        public void onAdd() {
+            super.onAdd();
+            listListEntry.referencableEntries.add(nestedEntry);
+            listListEntry.requestReferenceRebuilding();
+        }
+        
+        @Override
+        public void onDelete() {
+            super.onDelete();
+            listListEntry.referencableEntries.remove(nestedEntry);
+            listListEntry.requestReferenceRebuilding();
+        }
     }
-    
 }

+ 4 - 2
src/main/java/me/shedaniel/clothconfig2/gui/entries/SubCategoryListEntry.java

@@ -3,6 +3,7 @@ package me.shedaniel.clothconfig2.gui.entries;
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
 import me.shedaniel.math.Rectangle;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
@@ -38,6 +39,7 @@ public class SubCategoryListEntry extends TooltipListEntry<List<AbstractConfigLi
         this.widget = new CategoryLabelWidget();
         this.children = Lists.newArrayList(widget);
         this.children.addAll(entries);
+        this.setReferencableEntries((List) entries);
     }
     
     @Override
@@ -80,8 +82,8 @@ public class SubCategoryListEntry extends TooltipListEntry<List<AbstractConfigLi
         drawTexture(matrices, x - 15, y + 4, 24, (widget.rectangle.contains(mouseX, mouseY) ? 18 : 0) + (expanded ? 9 : 0), 9, 9);
         MinecraftClient.getInstance().textRenderer.drawWithShadow(matrices, getDisplayedFieldName(), x, y + 5, widget.rectangle.contains(mouseX, mouseY) ? 0xffe6fe16 : -1);
         for (AbstractConfigListEntry<?> entry : entries) {
-            entry.setParent(getParent());
-            entry.setScreen(getScreen());
+            entry.setParent((DynamicEntryListWidget) getParent());
+            entry.setScreen(getConfigScreen());
         }
         if (expanded) {
             int yy = y + 24;

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

@@ -1,7 +1,7 @@
 package me.shedaniel.clothconfig2.gui.entries;
 
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
-import me.shedaniel.clothconfig2.api.QueuedTooltip;
+import me.shedaniel.clothconfig2.api.Tooltip;
 import me.shedaniel.math.Point;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
@@ -36,7 +36,7 @@ public abstract class TooltipListEntry<T> extends AbstractConfigListEntry<T> {
         if (isMouseInside(mouseX, mouseY, x, y, entryWidth, entryHeight)) {
             Optional<Text[]> tooltip = getTooltip();
             if (tooltip.isPresent() && tooltip.get().length > 0)
-                getScreen().queueTooltip(QueuedTooltip.create(new Point(mouseX, mouseY), tooltip.get()));
+                addTooltip(Tooltip.of(new Point(mouseX, mouseY), tooltip.get()));
         }
     }
     

+ 3 - 4
src/main/java/me/shedaniel/clothconfig2/gui/widget/DynamicEntryListWidget.java

@@ -3,7 +3,6 @@ package me.shedaniel.clothconfig2.gui.widget;
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
-import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget.Entries;
 import me.shedaniel.math.Rectangle;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
@@ -39,7 +38,7 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     public int left;
     protected boolean verticallyCenter = true;
     protected int yDrag = -2;
-    protected boolean visible = true;
+    protected boolean selectionVisible = true;
     protected boolean renderSelection;
     protected int headerHeight;
     protected double scroll;
@@ -59,7 +58,7 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
     }
     
     public void setRenderSelection(boolean boolean_1) {
-        this.visible = boolean_1;
+        this.selectionVisible = boolean_1;
     }
     
     protected void setRenderHeader(boolean boolean_1, int headerHeight) {
@@ -411,7 +410,7 @@ public abstract class DynamicEntryListWidget<E extends DynamicEntryListWidget.En
             int itemHeight = item.getItemHeight() - 4;
             int itemWidth = this.getItemWidth();
             int itemMinX, itemMaxX;
-            if (this.visible && this.isSelected(renderIndex)) {
+            if (this.selectionVisible && this.isSelected(renderIndex)) {
                 itemMinX = this.left + this.width / 2 - itemWidth / 2;
                 itemMaxX = itemMinX + itemWidth;
                 RenderSystem.disableTexture();

+ 45 - 18
src/main/java/me/shedaniel/clothconfig2/impl/ConfigBuilderImpl.java

@@ -4,7 +4,10 @@ import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import me.shedaniel.clothconfig2.api.ConfigBuilder;
 import me.shedaniel.clothconfig2.api.ConfigCategory;
+import me.shedaniel.clothconfig2.api.TabbedConfigScreen;
+import me.shedaniel.clothconfig2.gui.AbstractConfigScreen;
 import me.shedaniel.clothconfig2.gui.ClothConfigScreen;
+import me.shedaniel.clothconfig2.gui.GlobalizedClothConfigScreen;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.gui.DrawableHelper;
@@ -12,20 +15,20 @@ import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.text.Text;
 import net.minecraft.text.TranslatableText;
 import net.minecraft.util.Identifier;
-import net.minecraft.util.Pair;
+import org.jetbrains.annotations.ApiStatus;
 
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Consumer;
 
-@Deprecated
 @Environment(EnvType.CLIENT)
+@ApiStatus.Internal
 public class ConfigBuilderImpl implements ConfigBuilder {
-    
     private Runnable savingRunnable;
     private Screen parent;
     private Text title = new TranslatableText("text.cloth-config.config");
+    private boolean globalized = false;
     private boolean editable = true;
     private boolean tabsSmoothScroll = true;
     private boolean listSmoothScroll = true;
@@ -34,15 +37,20 @@ public class ConfigBuilderImpl implements ConfigBuilder {
     private Identifier defaultBackground = DrawableHelper.BACKGROUND_TEXTURE;
     private Consumer<Screen> afterInitConsumer = screen -> {};
     private final Map<Text, Identifier> categoryBackground = Maps.newHashMap();
-    private final Map<Text, List<Pair<Text, Object>>> dataMap = Maps.newLinkedHashMap();
+    private final Map<Text, List<Object>> dataMap = Maps.newLinkedHashMap();
     private Text fallbackCategory = null;
     private boolean alwaysShowTabs = false;
     
-    @Deprecated
+    @ApiStatus.Internal
     public ConfigBuilderImpl() {
         
     }
     
+    @Override
+    public void setGlobalized(boolean globalized) {
+        this.globalized = globalized;
+    }
+    
     @Override
     public boolean isAlwaysShowTabs() {
         return alwaysShowTabs;
@@ -205,24 +213,43 @@ public class ConfigBuilderImpl implements ConfigBuilder {
     public Screen build() {
         if (dataMap.isEmpty() || fallbackCategory == null)
             throw new NullPointerException("There cannot be no categories or fallback category!");
-        ClothConfigScreen screen = new ClothConfigScreen(parent, title, dataMap, doesConfirmSave, defaultBackground) {
-            @Override
-            public void save() {
-                if (savingRunnable != null)
-                    savingRunnable.run();
-            }
-            
-            @Override
-            protected void init() {
-                super.init();
-                afterInitConsumer.accept(this);
-            }
+        AbstractConfigScreen screen;
+        if (globalized) {
+            screen = new GlobalizedClothConfigScreen(parent, title, dataMap, defaultBackground) {
+                @Override
+                public void save() {
+                    if (savingRunnable != null)
+                        savingRunnable.run();
+                }
+        
+                @Override
+                protected void init() {
+                    super.init();
+                    afterInitConsumer.accept(this);
+                }
+            };
+        } else {
+            screen = new ClothConfigScreen(parent, title, dataMap, defaultBackground) {
+                @Override
+                public void save() {
+                    if (savingRunnable != null)
+                        savingRunnable.run();
+                }
+        
+                @Override
+                protected void init() {
+                    super.init();
+                    afterInitConsumer.accept(this);
+                }
+            };
         };
         screen.setEditable(editable);
         screen.setFallbackCategory(fallbackCategory);
         screen.setTransparentBackground(transparentBackground);
         screen.setAlwaysShowTabs(alwaysShowTabs);
-        categoryBackground.forEach(screen::registerCategoryBackground);
+        screen.setConfirmSave(doesConfirmSave);
+        if (screen instanceof TabbedConfigScreen)
+            categoryBackground.forEach(((TabbedConfigScreen) screen)::registerCategoryBackground);
         return screen;
     }
     

+ 4 - 6
src/main/java/me/shedaniel/clothconfig2/impl/ConfigCategoryImpl.java

@@ -6,22 +6,20 @@ import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
-import net.minecraft.util.Pair;
 
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 @Environment(EnvType.CLIENT)
 public class ConfigCategoryImpl implements ConfigCategory {
     
-    private final Supplier<List<Pair<Text, Object>>> listSupplier;
+    private final Supplier<List<Object>> listSupplier;
     private final Consumer<Identifier> backgroundConsumer;
     private final Runnable destroyCategory;
     private final Text categoryKey;
     
-    ConfigCategoryImpl(Text categoryKey, Consumer<Identifier> backgroundConsumer, Supplier<List<Pair<Text, Object>>> listSupplier, Runnable destroyCategory) {
+    ConfigCategoryImpl(Text categoryKey, Consumer<Identifier> backgroundConsumer, Supplier<List<Object>> listSupplier, Runnable destroyCategory) {
         this.listSupplier = listSupplier;
         this.backgroundConsumer = backgroundConsumer;
         this.categoryKey = categoryKey;
@@ -35,12 +33,12 @@ public class ConfigCategoryImpl implements ConfigCategory {
     
     @Override
     public List<Object> getEntries() {
-        return listSupplier.get().stream().map(Pair::getRight).collect(Collectors.toList());
+        return listSupplier.get();
     }
     
     @Override
     public ConfigCategory addEntry(AbstractConfigListEntry entry) {
-        listSupplier.get().add(new Pair<>(null, entry));
+        listSupplier.get().add(entry);
         return this;
     }
     

+ 0 - 51
src/main/java/me/shedaniel/clothconfig2/impl/FakeModifierKeyCodeAdderImpl.java

@@ -1,51 +0,0 @@
-package me.shedaniel.clothconfig2.impl;
-
-import me.shedaniel.clothconfig2.api.FakeModifierKeyCodeAdder;
-import me.shedaniel.clothconfig2.api.ModifierKeyCode;
-import net.fabricmc.api.EnvType;
-import net.fabricmc.api.Environment;
-import net.fabricmc.loader.api.FabricLoader;
-import net.minecraft.client.options.KeyBinding;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-@Environment(EnvType.CLIENT)
-public class FakeModifierKeyCodeAdderImpl implements FakeModifierKeyCodeAdder {
-    private final List<Entry> entryList = new ArrayList<>();
-    
-    @Override
-    public void registerModifierKeyCode(String category, String translationKey, Supplier<ModifierKeyCode> keyCode, Supplier<ModifierKeyCode> defaultKeyCode, Consumer<ModifierKeyCode> onChanged) {
-        entryList.add(new Entry(category, translationKey, keyCode, defaultKeyCode, onChanged));
-    }
-    
-    @Override
-    public List<KeyBinding> getFakeBindings() {
-        if (FabricLoader.getInstance().isModLoaded("amecs"))
-            return Collections.emptyList();
-        List<KeyBinding> keyBindings = new ArrayList<>();
-        for (Entry entry : entryList) {
-            keyBindings.add(new FakeKeyBindings(entry.translationKey, entry.keyCode.get(), entry.defaultKeyCode.get(), entry.category, entry.onChanged));
-        }
-        return keyBindings;
-    }
-    
-    private class Entry {
-        private String category;
-        private String translationKey;
-        private Supplier<ModifierKeyCode> keyCode;
-        private Supplier<ModifierKeyCode> defaultKeyCode;
-        private Consumer<ModifierKeyCode> onChanged;
-        
-        private Entry(String category, String translationKey, Supplier<ModifierKeyCode> keyCode, Supplier<ModifierKeyCode> defaultKeyCode, Consumer<ModifierKeyCode> onChanged) {
-            this.category = category;
-            this.translationKey = translationKey;
-            this.keyCode = keyCode;
-            this.defaultKeyCode = defaultKeyCode;
-            this.onChanged = onChanged;
-        }
-    }
-}

+ 0 - 52
src/main/java/me/shedaniel/clothconfig2/mixin/MixinControlsOptionsScreen.java

@@ -1,52 +0,0 @@
-package me.shedaniel.clothconfig2.mixin;
-
-import me.shedaniel.clothconfig2.api.FakeModifierKeyCodeAdder;
-import me.shedaniel.clothconfig2.impl.FakeKeyBindings;
-import me.shedaniel.clothconfig2.impl.GameOptionsHooks;
-import net.minecraft.client.gui.screen.Screen;
-import net.minecraft.client.gui.screen.options.ControlsOptionsScreen;
-import net.minecraft.client.gui.screen.options.GameOptionsScreen;
-import net.minecraft.client.options.GameOptions;
-import net.minecraft.client.options.KeyBinding;
-import net.minecraft.text.Text;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Mixin(ControlsOptionsScreen.class)
-public class MixinControlsOptionsScreen extends GameOptionsScreen {
-    public MixinControlsOptionsScreen(Screen screen_1, GameOptions gameOptions_1, Text text_1) {
-        super(screen_1, gameOptions_1, text_1);
-    }
-    
-    @Inject(method = "init()V", at = @At("HEAD"))
-    private void initHead(CallbackInfo info) {
-        List<KeyBinding> newKeysAll = new ArrayList<>();
-        KeyBinding[] var3 = client.options.keysAll;
-        
-        for (KeyBinding binding : var3) {
-            if (!(binding instanceof FakeKeyBindings)) {
-                newKeysAll.add(binding);
-            }
-        }
-        
-        newKeysAll.addAll(FakeModifierKeyCodeAdder.INSTANCE.getFakeBindings());
-        ((GameOptionsHooks) client.options).cloth_setKeysAll(newKeysAll.toArray(new KeyBinding[0]));
-    }
-    
-    @Inject(method = "init()V", at = @At("RETURN"))
-    private void initReturn(CallbackInfo info) {
-        List<KeyBinding> newKeysAll = new ArrayList<>();
-        KeyBinding[] var3 = client.options.keysAll;
-        for (KeyBinding binding : var3) {
-            if (!(binding instanceof FakeKeyBindings)) {
-                newKeysAll.add(binding);
-            }
-        }
-        ((GameOptionsHooks) client.options).cloth_setKeysAll(newKeysAll.toArray(new KeyBinding[0]));
-    }
-}

+ 0 - 19
src/main/java/me/shedaniel/clothconfig2/mixin/MixinGameOptions.java

@@ -1,19 +0,0 @@
-package me.shedaniel.clothconfig2.mixin;
-
-import me.shedaniel.clothconfig2.impl.GameOptionsHooks;
-import net.minecraft.client.options.GameOptions;
-import net.minecraft.client.options.KeyBinding;
-import org.spongepowered.asm.mixin.Final;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Mutable;
-import org.spongepowered.asm.mixin.Shadow;
-
-@Mixin(GameOptions.class)
-public class MixinGameOptions implements GameOptionsHooks {
-    @Shadow @Mutable @Final public KeyBinding[] keysAll;
-    
-    @Override
-    public void cloth_setKeysAll(KeyBinding[] all) {
-        keysAll = all;
-    }
-}

+ 0 - 18
src/main/java/me/shedaniel/clothconfig2/mixin/MixinKeyBinding.java

@@ -1,18 +0,0 @@
-package me.shedaniel.clothconfig2.mixin;
-
-import me.shedaniel.clothconfig2.impl.KeyBindingHooks;
-import net.minecraft.client.options.KeyBinding;
-import org.spongepowered.asm.mixin.Final;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Mutable;
-import org.spongepowered.asm.mixin.Shadow;
-
-@Mixin(KeyBinding.class)
-public class MixinKeyBinding implements KeyBindingHooks {
-    @Shadow @Mutable @Final private String id;
-    
-    @Override
-    public void cloth_setId(String id) {
-        this.id = id;
-    }
-}

+ 1 - 1
src/main/resources/mixin.cloth-config2.json

@@ -3,7 +3,7 @@
   "package": "me.shedaniel.clothconfig2.mixin",
   "minVersion": "0.7.11",
   "compatibilityLevel": "JAVA_8",
-  "client": ["ButtonWidgetHooks", "MixinControlsOptionsScreen", "MixinGameOptions", "MixinKeyBinding"],
+  "client": ["ButtonWidgetHooks"],
   "injectors": {
     "defaultRequire": 1
   }