Parcourir la source

Rewritten Config Screen

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel il y a 5 ans
Parent
commit
97688c0d62
41 fichiers modifiés avec 1103 ajouts et 734 suppressions
  1. 10 7
      build.gradle
  2. 6 6
      gradle.properties
  3. 4 4
      src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java
  4. 2 6
      src/main/java/me/shedaniel/rei/api/ConfigObject.java
  5. 2 3
      src/main/java/me/shedaniel/rei/gui/PreRecipeViewingScreen.java
  6. 2 3
      src/main/java/me/shedaniel/rei/gui/RecipeDisplayExporter.java
  7. 10 10
      src/main/java/me/shedaniel/rei/gui/WarningAndErrorScreen.java
  8. 40 0
      src/main/java/me/shedaniel/rei/gui/config/AppearanceTheme.java
  9. 40 0
      src/main/java/me/shedaniel/rei/gui/config/ConfigButtonPosition.java
  10. 40 0
      src/main/java/me/shedaniel/rei/gui/config/DisplayPanelLocation.java
  11. 5 5
      src/main/java/me/shedaniel/rei/gui/config/EntryPanelOrdering.java
  12. 11 11
      src/main/java/me/shedaniel/rei/gui/config/EntryPanelOrderingConfig.java
  13. 1 1
      src/main/java/me/shedaniel/rei/gui/config/SearchFieldLocation.java
  14. 26 394
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringEntry.java
  15. 510 0
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringScreen.java
  16. 17 12
      src/main/java/me/shedaniel/rei/gui/config/entry/NoFilteringEntry.java
  17. 4 4
      src/main/java/me/shedaniel/rei/gui/config/entry/RecipeScreenTypeEntry.java
  18. 94 0
      src/main/java/me/shedaniel/rei/gui/config/entry/ReloadPluginsEntry.java
  19. 3 3
      src/main/java/me/shedaniel/rei/gui/credits/CreditsEntryListWidget.java
  20. 4 5
      src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java
  21. 7 7
      src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java
  22. 10 19
      src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java
  23. 148 123
      src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java
  24. 1 1
      src/main/java/me/shedaniel/rei/impl/ScreenHelper.java
  25. 5 5
      src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationCategory.java
  26. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/bg_bg.json
  27. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/de_de.json
  28. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/en_ud.json
  29. 71 75
      src/main/resources/assets/roughlyenoughitems/lang/en_us.json
  30. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/et_ee.json
  31. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/fr_fr.json
  32. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/ja_jp.json
  33. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/lol_us.json
  34. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/nn_no.json
  35. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/pl_pl.json
  36. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/pt_br.json
  37. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/pt_pt.json
  38. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/ru_ru.json
  39. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/zh_cn.json
  40. 2 2
      src/main/resources/assets/roughlyenoughitems/lang/zh_tw.json
  41. 2 2
      src/main/resources/fabric.mod.json

+ 10 - 7
build.gradle

@@ -30,6 +30,7 @@ license {
 
 repositories {
     maven { url "https://dl.bintray.com/shedaniel/legacy-yarn-updated" }
+    maven { url "https://dl.bintray.com/shedaniel/shedaniel-mods" }
 }
 
 jar {
@@ -48,24 +49,26 @@ dependencies {
     mappings("me.shedaniel:legacy-yarn:${project.yarn_version}:v2")
     modApi("net.fabricmc:fabric-loader:${project.fabricloader_version}")
     modApi("net.fabricmc.fabric-api:fabric-api:${project.fabric_api}") {
-        exclude(group: "net.fabricmc")
         exclude(module: "fabric-biomes-v1")
     }
-    modApi("me.shedaniel.cloth:cloth-events:${cloth_events_version}") {
-        transitive = false
+    modApi("me.shedaniel.cloth.api:cloth-client-events-v0:${cloth_client_events_v0_version}") {
+        transitive(false)
     }
     modApi("me.shedaniel.cloth:config-2:${cloth_config_version}") {
-        exclude module: "fabric-api"
+        exclude(module: "fabric-api")
     }
     modApi("me.sargunvohra.mcmods:autoconfig1u:${project.autoconfig1u}") {
-        exclude module: "fabric-api"
+        exclude(module: "fabric-api")
     }
     modApi("org.jetbrains:annotations:19.0.0")
     modCompileOnly("io.github.prospector:modmenu:${modmenu_version}") {
-        transitive = false
+        transitive(false)
     }
     modRuntime("io.github.prospector:modmenu:${modmenu_version}") {
-        transitive = false
+        transitive(false)
+    }
+    modRuntime("me.shedaniel:SmoothScrollingEverywhere:3.0.3-unstable") {
+        transitive(false)
     }
     afterEvaluate {
         def listAdded = new ArrayList(Arrays.asList((api_exculde as String).split(',')))

+ 6 - 6
gradle.properties

@@ -1,14 +1,14 @@
 org.gradle.jvmargs=-Xmx3G
-mod_version=4.4.3
+mod_version=4.5.0
 supported_version=1.16-pre3
 minecraft_version=1.16-pre3
 yarn_version=1.16-pre3+build.1+legacy.20w09a+build.8
 fabricloader_version=0.8.7+build.201
-cloth_events_version=2.2.0-unstable
-cloth_config_version=4.5.2
+cloth_client_events_v0_version=1.0.2
+cloth_config_version=4.5.3
 modmenu_version=1.12.0+build.14
-fabric_api=0.11.7+build.356-1.16
-autoconfig1u=3.2.1-unstable
-api_include=me.shedaniel.cloth:cloth-events,me.shedaniel.cloth:config-2,me.sargunvohra.mcmods:autoconfig1u
+fabric_api=0.11.8+build.357-1.16
+autoconfig1u=3.2.0-unstable
+api_include=me.shedaniel.cloth.api:cloth-client-events-v0,me.shedaniel.cloth:config-2,me.sargunvohra.mcmods:autoconfig1u,net.fabricmc.fabric-api:fabric-api-base
 api_exculde=
 #api_include=me.shedaniel.cloth:cloth-events,me.shedaniel.cloth:config-2,me.sargunvohra.mcmods:autoconfig1u,org.jetbrains:annotations,net.fabricmc.fabric-api:fabric-

+ 4 - 4
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java

@@ -25,7 +25,7 @@ package me.shedaniel.rei;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import me.shedaniel.cloth.hooks.ClothClientHooks;
+import me.shedaniel.cloth.api.client.events.v0.ClothClientHooks;
 import me.shedaniel.math.api.Executor;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
@@ -321,14 +321,14 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
             if (screen instanceof ContainerScreen)
                 ScreenHelper.setPreviousContainerScreen((ContainerScreen<?>) screen);
             boolean alreadyAdded = false;
-            for (Element element : Lists.newArrayList(screenHooks.cloth_getChildren()))
+            for (Element element : Lists.newArrayList(screenHooks.cloth$getChildren()))
                 if (ContainerScreenOverlay.class.isAssignableFrom(element.getClass()))
                     if (alreadyAdded)
-                        screenHooks.cloth_getChildren().remove(element);
+                        screenHooks.cloth$getChildren().remove(element);
                     else
                         alreadyAdded = true;
             if (!alreadyAdded)
-                screenHooks.cloth_getChildren().add(ScreenHelper.getLastOverlay(true, false));
+                screenHooks.cloth$getChildren().add(ScreenHelper.getLastOverlay(true, false));
         });
         ClothClientHooks.SCREEN_RENDER_POST.register((matrices, minecraftClient, screen, i, i1, v) -> {
             if (shouldReturn(screen))

+ 2 - 6
src/main/java/me/shedaniel/rei/api/ConfigObject.java

@@ -24,12 +24,11 @@
 package me.shedaniel.rei.api;
 
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
-import me.shedaniel.rei.gui.config.ItemListOrdering;
+import me.shedaniel.rei.gui.config.EntryPanelOrdering;
 import me.shedaniel.rei.gui.config.RecipeBorderType;
 import me.shedaniel.rei.gui.config.RecipeScreenType;
 import me.shedaniel.rei.gui.config.SearchFieldLocation;
 import me.shedaniel.rei.impl.ConfigManagerImpl;
-import me.shedaniel.rei.impl.ConfigObjectImpl;
 import org.jetbrains.annotations.ApiStatus;
 
 import java.util.List;
@@ -51,7 +50,7 @@ public interface ConfigObject {
     
     void setCheating(boolean cheating);
     
-    ItemListOrdering getItemListOrdering();
+    EntryPanelOrdering getItemListOrdering();
     
     boolean isItemListAscending();
     
@@ -137,9 +136,6 @@ public interface ConfigObject {
     
     double getEntrySize();
     
-    @ApiStatus.Internal
-    ConfigObjectImpl.General getGeneral();
-    
     boolean isUsingCompactTabs();
     
     boolean isLowerConfigButton();

+ 2 - 3
src/main/java/me/shedaniel/rei/gui/PreRecipeViewingScreen.java

@@ -34,14 +34,13 @@ import me.shedaniel.rei.gui.config.RecipeScreenType;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.impl.ScreenHelper;
-import net.minecraft.class_5348;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.screen.ingame.ContainerScreen;
 import net.minecraft.client.util.NarratorManager;
 import net.minecraft.client.util.math.MatrixStack;
-import net.minecraft.text.Text;
+import net.minecraft.text.StringRenderable;
 import net.minecraft.text.TranslatableText;
 import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
@@ -128,7 +127,7 @@ public class PreRecipeViewingScreen extends Screen {
         this.drawCenteredText(matrices, this.textRenderer, this.title, this.width / 2, 20, 16777215);
         if (showTips) {
             int i = 30;
-            for (class_5348 s : this.textRenderer.wrapStringToWidthAsList(new TranslatableText("text.rei.recipe_screen_type.selection.sub").formatted(Formatting.GRAY), width - 30)) {
+            for (StringRenderable s : this.textRenderer.wrapStringToWidthAsList(new TranslatableText("text.rei.recipe_screen_type.selection.sub").formatted(Formatting.GRAY), width - 30)) {
                 this.drawCenteredText(matrices, this.textRenderer, s, width / 2, i, -1);
                 i += 10;
             }

+ 2 - 3
src/main/java/me/shedaniel/rei/gui/RecipeDisplayExporter.java

@@ -23,7 +23,6 @@
 
 package me.shedaniel.rei.gui;
 
-import com.mojang.blaze3d.platform.GlStateManager;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.gui.toast.ExportRecipeIdentifierToast;
@@ -36,7 +35,7 @@ import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.texture.NativeImage;
 import net.minecraft.client.util.Window;
 import net.minecraft.client.util.math.MatrixStack;
-import net.minecraft.resource.ResourceImpl;
+import net.minecraft.util.Util;
 import org.jetbrains.annotations.ApiStatus;
 
 import java.io.File;
@@ -105,7 +104,7 @@ public final class RecipeDisplayExporter extends Widget {
                 strippedImage.setPixelRgba(x, y, nativeImage.getPixelRgba(x + (int) (rectangle.x * window.getScaleFactor()), y + (int) (rectangle.y * window.getScaleFactor())));
             }
         }
-        ResourceImpl.RESOURCE_IO_EXECUTOR.execute(() -> {
+        Util.method_27958().execute(() -> {
             try {
                 File export = new File(minecraft.runDirectory, "rei_exports");
                 export.mkdirs();

+ 10 - 10
src/main/java/me/shedaniel/rei/gui/WarningAndErrorScreen.java

@@ -25,7 +25,6 @@ package me.shedaniel.rei.gui;
 
 import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
 import me.shedaniel.rei.RoughlyEnoughItemsState;
-import net.minecraft.class_5348;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.DrawableHelper;
 import net.minecraft.client.gui.screen.Screen;
@@ -37,6 +36,7 @@ import net.minecraft.client.util.TextCollector;
 import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.sound.SoundEvents;
 import net.minecraft.text.LiteralText;
+import net.minecraft.text.StringRenderable;
 import net.minecraft.text.Style;
 import net.minecraft.text.Text;
 import net.minecraft.util.Formatting;
@@ -70,13 +70,13 @@ public class WarningAndErrorScreen extends Screen {
     }
     
     private void addText(Text string) {
-        for (class_5348 s : textRenderer.wrapStringToWidthAsList(string, width - 80)) {
+        for (StringRenderable s : textRenderer.wrapStringToWidthAsList(string, width - 80)) {
             listWidget.creditsAddEntry(new TextItem(s));
         }
     }
     
     private void addLink(Text string, String link) {
-        for (class_5348 s : textRenderer.wrapStringToWidthAsList(string, width - 80)) {
+        for (StringRenderable s : textRenderer.wrapStringToWidthAsList(string, width - 80)) {
             listWidget.creditsAddEntry(new LinkItem(s, link));
         }
     }
@@ -214,9 +214,9 @@ public class WarningAndErrorScreen extends Screen {
     }
     
     private static class TextItem extends StringItem {
-        private class_5348 text;
+        private StringRenderable text;
         
-        public TextItem(class_5348 text) {
+        public TextItem(StringRenderable text) {
             this.text = text;
         }
         
@@ -242,11 +242,11 @@ public class WarningAndErrorScreen extends Screen {
     }
     
     private class LinkItem extends StringItem {
-        private class_5348 text;
+        private StringRenderable text;
         private String link;
         private boolean contains;
         
-        public LinkItem(class_5348 text, String link) {
+        public LinkItem(StringRenderable text, String link) {
             this.text = text;
             this.link = link;
         }
@@ -256,12 +256,12 @@ public class WarningAndErrorScreen extends Screen {
             contains = mouseX >= x && mouseX <= x + entryWidth && mouseY >= y && mouseY <= y + entryHeight;
             if (contains) {
                 WarningAndErrorScreen.this.renderTooltip(matrices, new LiteralText("Click to open link."), mouseX, mouseY);
-                class_5348 underlined = text.visit(new class_5348.StyledVisitor<class_5348>() {
+                StringRenderable underlined = text.visit(new StringRenderable.StyledVisitor<StringRenderable>() {
                     TextCollector collector = new TextCollector();
                     
                     @Override
-                    public Optional<class_5348> accept(Style style, String asString) {
-                        collector.add(class_5348.method_29431(asString, style));
+                    public Optional<StringRenderable> accept(Style style, String asString) {
+                        collector.add(StringRenderable.styled(asString, style));
                         return Optional.of(collector.getCombined());
                     }
                 }, Style.EMPTY.withFormatting(Formatting.UNDERLINE)).orElse(text);

+ 40 - 0
src/main/java/me/shedaniel/rei/gui/config/AppearanceTheme.java

@@ -0,0 +1,40 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config;
+
+import me.shedaniel.clothconfig2.gui.entries.SelectionListEntry;
+import net.minecraft.client.resource.language.I18n;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Locale;
+
+public enum AppearanceTheme implements SelectionListEntry.Translatable {
+    LIGHT,
+    DARK;
+    
+    @Override
+    public @NotNull String getKey() {
+        return I18n.translate("config.roughlyenoughitems.theme." + name().toLowerCase(Locale.ROOT));
+    }
+}

+ 40 - 0
src/main/java/me/shedaniel/rei/gui/config/ConfigButtonPosition.java

@@ -0,0 +1,40 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config;
+
+import me.shedaniel.clothconfig2.gui.entries.SelectionListEntry;
+import net.minecraft.client.resource.language.I18n;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Locale;
+
+public enum ConfigButtonPosition implements SelectionListEntry.Translatable {
+    UPPER,
+    LOWER;
+    
+    @Override
+    public @NotNull String getKey() {
+        return I18n.translate("config.roughlyenoughitems.layout.configButtonLocation." + name().toLowerCase(Locale.ROOT));
+    }
+}

+ 40 - 0
src/main/java/me/shedaniel/rei/gui/config/DisplayPanelLocation.java

@@ -0,0 +1,40 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config;
+
+import me.shedaniel.clothconfig2.gui.entries.SelectionListEntry;
+import net.minecraft.client.resource.language.I18n;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Locale;
+
+public enum DisplayPanelLocation implements SelectionListEntry.Translatable {
+    LEFT,
+    RIGHT;
+    
+    @Override
+    public @NotNull String getKey() {
+        return I18n.translate("config.roughlyenoughitems.accessibility.displayPanelLocation." + name().toLowerCase(Locale.ROOT));
+    }
+}

+ 5 - 5
src/main/java/me/shedaniel/rei/gui/config/ItemListOrdering.java → src/main/java/me/shedaniel/rei/gui/config/EntryPanelOrdering.java

@@ -26,15 +26,15 @@ package me.shedaniel.rei.gui.config;
 import org.jetbrains.annotations.ApiStatus;
 
 @ApiStatus.Internal
-public enum ItemListOrdering {
+public enum EntryPanelOrdering {
     
-    registry("ordering.rei.registry"),
-    name("ordering.rei.name"),
-    item_groups("ordering.rei.item_groups");
+    REGISTRY("ordering.rei.registry"),
+    NAME("ordering.rei.name"),
+    GROUPS("ordering.rei.item_groups");
     
     private String nameTranslationKey;
     
-    ItemListOrdering(String nameTranslationKey) {
+    EntryPanelOrdering(String nameTranslationKey) {
         this.nameTranslationKey = nameTranslationKey;
     }
     

+ 11 - 11
src/main/java/me/shedaniel/rei/gui/config/ItemListOrderingConfig.java → src/main/java/me/shedaniel/rei/gui/config/EntryPanelOrderingConfig.java

@@ -27,28 +27,28 @@ import net.minecraft.client.resource.language.I18n;
 import org.jetbrains.annotations.ApiStatus;
 
 @ApiStatus.Internal
-public enum ItemListOrderingConfig {
-    REGISTRY_ASCENDING(ItemListOrdering.registry, true),
-    NAME_ASCENDING(ItemListOrdering.name, true),
-    GROUPS_ASCENDING(ItemListOrdering.item_groups, true),
-    REGISTRY_DESCENDING(ItemListOrdering.registry, false),
-    NAME_DESCENDING(ItemListOrdering.name, false),
-    GROUPS_DESCENDING(ItemListOrdering.item_groups, false);
+public enum EntryPanelOrderingConfig {
+    REGISTRY_ASCENDING(EntryPanelOrdering.REGISTRY, true),
+    NAME_ASCENDING(EntryPanelOrdering.NAME, true),
+    GROUPS_ASCENDING(EntryPanelOrdering.GROUPS, true),
+    REGISTRY_DESCENDING(EntryPanelOrdering.REGISTRY, false),
+    NAME_DESCENDING(EntryPanelOrdering.NAME, false),
+    GROUPS_DESCENDING(EntryPanelOrdering.GROUPS, false);
     
-    private ItemListOrdering ordering;
+    private EntryPanelOrdering ordering;
     private boolean isAscending;
     
-    ItemListOrderingConfig(ItemListOrdering ordering, boolean isAscending) {
+    EntryPanelOrderingConfig(EntryPanelOrdering ordering, boolean isAscending) {
         this.ordering = ordering;
         this.isAscending = isAscending;
     }
     
-    public static ItemListOrderingConfig from(ItemListOrdering ordering, boolean isAscending) {
+    public static EntryPanelOrderingConfig from(EntryPanelOrdering ordering, boolean isAscending) {
         int index = ordering.ordinal() + (isAscending ? 0 : 3);
         return values()[index];
     }
     
-    public ItemListOrdering getOrdering() {
+    public EntryPanelOrdering getOrdering() {
         return ordering;
     }
     

+ 1 - 1
src/main/java/me/shedaniel/rei/gui/config/SearchFieldLocation.java

@@ -36,6 +36,6 @@ public enum SearchFieldLocation {
     
     @Override
     public String toString() {
-        return I18n.translate("config.roughlyenoughitems.searchFieldLocation.%s", name().toLowerCase(Locale.ROOT));
+        return I18n.translate("config.roughlyenoughitems.layout.searchFieldLocation.%s", name().toLowerCase(Locale.ROOT));
     }
 }

+ 26 - 394
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringEntry.java

@@ -23,148 +23,43 @@
 
 package me.shedaniel.rei.gui.config.entry;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.mojang.blaze3d.systems.RenderSystem;
-import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import com.google.common.collect.ImmutableList;
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
-import me.shedaniel.clothconfig2.api.ScrollingContainer;
-import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
-import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
-import me.shedaniel.math.Point;
-import me.shedaniel.math.Rectangle;
-import me.shedaniel.math.impl.PointHelper;
-import me.shedaniel.rei.api.ConfigObject;
-import me.shedaniel.rei.api.EntryRegistry;
 import me.shedaniel.rei.api.EntryStack;
-import me.shedaniel.rei.api.REIHelper;
-import me.shedaniel.rei.api.widgets.Tooltip;
-import me.shedaniel.rei.gui.OverlaySearchField;
-import me.shedaniel.rei.gui.widget.EntryWidget;
-import me.shedaniel.rei.impl.ScreenHelper;
-import me.shedaniel.rei.impl.SearchArgument;
-import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
-import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.AbstractButtonWidget;
 import net.minecraft.client.gui.widget.ButtonWidget;
 import net.minecraft.client.util.NarratorManager;
+import net.minecraft.client.util.Window;
 import net.minecraft.client.util.math.MatrixStack;
-import net.minecraft.text.Text;
 import net.minecraft.text.TranslatableText;
-import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.Nullable;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 import java.util.function.Consumer;
 
-import static me.shedaniel.rei.gui.widget.EntryListWidget.entrySize;
-
 @ApiStatus.Internal
 public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
-    protected List<EntryStack> selected = Lists.newArrayList();
-    protected final ScrollingContainer scrolling = new ScrollingContainer() {
-        @Override
-        public int getMaxScrollHeight() {
-            return MathHelper.ceil(entryStacks.size() / (innerBounds.width / (float) entrySize())) * entrySize() + 28;
-        }
-        
-        @Override
-        public Rectangle getBounds() {
-            return FilteringEntry.this.getBounds();
-        }
-        
-        @Override
-        public int getScrollBarX() {
-            return getParent().right - 7;
-        }
-    };
-    private Consumer<List<EntryStack>> saveConsumer;
-    private List<EntryStack> defaultValue;
-    private List<EntryStack> configFiltered;
-    private Tooltip tooltip = null;
-    @SuppressWarnings("rawtypes") private DynamicEntryListWidget lastList = null;
-    private List<EntryStack> entryStacks = null;
-    private Rectangle innerBounds;
-    private List<EntryListEntry> entries = Collections.emptyList();
-    private List<Element> elements = Collections.emptyList();
-    
-    private Point selectionPoint = null;
-    private Point secondPoint = null;
-    
-    private OverlaySearchField searchField;
-    private ButtonWidget selectAllButton;
-    private ButtonWidget selectNoneButton;
-    private ButtonWidget hideButton;
-    private ButtonWidget showButton;
-    
-    private boolean edited = false;
-    
-    private List<SearchArgument.SearchArguments> lastSearchArguments = Collections.emptyList();
-    
-    public FilteringEntry(List<EntryStack> configFiltered, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer) {
+    private int width;
+    Consumer<List<EntryStack>> saveConsumer;
+    List<EntryStack> defaultValue;
+    List<EntryStack> configFiltered;
+    boolean edited = false;
+    private final FilteringScreen filteringScreen = new FilteringScreen(this);
+    private final AbstractButtonWidget buttonWidget = new ButtonWidget(0, 0, 0, 20, new TranslatableText("config.roughlyenoughitems.filteringScreen"), button -> {
+        filteringScreen.parent = MinecraftClient.getInstance().currentScreen;
+        MinecraftClient.getInstance().openScreen(filteringScreen);
+    });
+    private final List<Element> children = ImmutableList.of(buttonWidget);
+    
+    public FilteringEntry(int width, List<EntryStack> configFiltered, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer) {
         super(NarratorManager.EMPTY, false);
+        this.width = width;
         this.configFiltered = configFiltered;
         this.defaultValue = defaultValue;
         this.saveConsumer = saveConsumer;
-        this.searchField = new OverlaySearchField(0, 0, 0, 0);
-        {
-            Text selectAllText = new TranslatableText("config.roughlyenoughitems.filteredEntries.selectAll");
-            this.selectAllButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(selectAllText) + 10, 20, selectAllText, button -> {
-                this.selectionPoint = new Point(-Integer.MAX_VALUE / 2, -Integer.MAX_VALUE / 2);
-                this.secondPoint = new Point(Integer.MAX_VALUE / 2, Integer.MAX_VALUE / 2);
-            });
-        }
-        {
-            Text selectNoneText = new TranslatableText("config.roughlyenoughitems.filteredEntries.selectNone");
-            this.selectNoneButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(selectNoneText) + 10, 20, selectNoneText, button -> {
-                this.selectionPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
-                this.secondPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
-            });
-        }
-        {
-            Text hideText = new TranslatableText("config.roughlyenoughitems.filteredEntries.hide");
-            this.hideButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(hideText) + 10, 20, hideText, button -> {
-                for (int i = 0; i < entryStacks.size(); i++) {
-                    EntryStack stack = entryStacks.get(i);
-                    EntryListEntry entry = entries.get(i);
-                    entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
-                    if (entry.isSelected() && !entry.isFiltered()) {
-                        configFiltered.add(stack);
-                        edited = true;
-                    }
-                }
-            });
-        }
-        {
-            Text showText = new TranslatableText("config.roughlyenoughitems.filteredEntries.show");
-            this.showButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(showText) + 10, 20, showText, button -> {
-                for (int i = 0; i < entryStacks.size(); i++) {
-                    EntryStack stack = entryStacks.get(i);
-                    EntryListEntry entry = entries.get(i);
-                    entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
-                    if (entry.isSelected() && configFiltered.remove(stack)) {
-                        edited = true;
-                    }
-                }
-            });
-        }
-        this.searchField.isMain = false;
-    }
-    
-    private static Rectangle updateInnerBounds(Rectangle bounds) {
-        int width = Math.max(MathHelper.floor((bounds.width - 2 - 6) / (float) entrySize()), 1);
-        return new Rectangle((int) (bounds.getCenterX() - width * entrySize() / 2f), bounds.y + 5, width * entrySize(), bounds.height);
-    }
-    
-    @SuppressWarnings("rawtypes")
-    public Rectangle getBounds() {
-        DynamicEntryListWidget listWidget = getParent();
-        return new Rectangle(listWidget.left, listWidget.top, listWidget.right - listWidget.left, listWidget.bottom - listWidget.top);
     }
     
     @Override
@@ -183,286 +78,23 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
         this.edited = false;
     }
     
-    @Override
-    public boolean isEdited() {
-        return super.isEdited() || edited;
-    }
-    
-    @SuppressWarnings("rawtypes")
     @Override
     public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
-        DynamicEntryListWidget parent = getParent();
-        Rectangle bounds = getBounds();
-        if (lastList != parent) {
-            updateSearch(this.searchField.getText());
-            lastList = parent;
-            this.searchField.getBounds().setBounds(bounds.getCenterX() - 75, bounds.getMaxY() - 22, 150, 18);
-            this.selectAllButton.x = 2;
-            this.selectAllButton.y = bounds.getMaxY() - 22;
-            this.selectNoneButton.x = 4 + selectAllButton.getWidth();
-            this.selectNoneButton.y = bounds.getMaxY() - 22;
-            this.hideButton.x = bounds.getMaxX() - hideButton.getWidth() - showButton.getWidth() - 4;
-            this.hideButton.y = bounds.getMaxY() - 22;
-            this.showButton.x = bounds.getMaxX() - showButton.getWidth() - 2;
-            this.showButton.y = bounds.getMaxY() - 22;
-            this.searchField.setChangedListener(this::updateSearch);
-        }
-        tooltip = null;
-        if (bounds.isEmpty())
-            return;
-        for (EntryListEntry entry : entries)
-            entry.clearStacks();
-        int skip = Math.max(0, MathHelper.floor(scrolling.scrollAmount / (float) entrySize()));
-        int nextIndex = skip * innerBounds.width / entrySize();
-        int i = nextIndex;
-        for (; i < entryStacks.size(); i++) {
-            EntryStack stack = entryStacks.get(i);
-            EntryListEntry entry = entries.get(nextIndex);
-            entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
-            if (entry.getBounds().y > bounds.getMaxY())
-                break;
-            entry.entry(stack);
-            entry.render(matrices, mouseX, mouseY, delta);
-            nextIndex++;
-        }
-        updatePosition(delta);
-        scrolling.renderScrollBar(0xff000000, 1, REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-        matrices.push();
-        matrices.translate(0, 0, 300);
-        this.searchField.laterRender(matrices, mouseX, mouseY, delta);
-        this.selectAllButton.render(matrices, mouseX, mouseY, delta);
-        this.selectNoneButton.render(matrices, mouseX, mouseY, delta);
-        this.hideButton.render(matrices, mouseX, mouseY, delta);
-        this.showButton.render(matrices, mouseX, mouseY, delta);
-        matrices.pop();
-        if (tooltip != null) {
-            ScreenHelper.getLastOverlay().renderTooltip(matrices, tooltip);
-        }
-    }
-    
-    private Rectangle getSelection() {
-        if (selectionPoint != null) {
-            Point p = secondPoint;
-            if (p == null) {
-                p = PointHelper.ofMouse();
-                p.translate(0, (int) scrolling.scrollAmount);
-            }
-            int left = Math.min(p.x, selectionPoint.x);
-            int top = Math.min(p.y, selectionPoint.y);
-            int right = Math.max(p.x, selectionPoint.x);
-            int bottom = Math.max(p.y, selectionPoint.y);
-            return new Rectangle(left, (int) (top - scrolling.scrollAmount), right - left, bottom - top);
-        }
-        return new Rectangle(0, 0, 0, 0);
-    }
-    
-    @Override
-    public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
-        if (scrolling.mouseDragged(mouseX, mouseY, button, dx, dy, ConfigObject.getInstance().doesSnapToRows(), entrySize()))
-            return true;
-        return super.mouseDragged(mouseX, mouseY, button, dx, dy);
-    }
-    
-    private void updatePosition(float delta) {
-        if (ConfigObject.getInstance().doesSnapToRows() && scrolling.scrollTarget >= 0 && scrolling.scrollTarget <= scrolling.getMaxScroll()) {
-            double nearestRow = Math.round(scrolling.scrollTarget / (double) entrySize()) * (double) entrySize();
-            if (!DynamicNewSmoothScrollingEntryListWidget.Precision.almostEquals(scrolling.scrollTarget, nearestRow, DynamicNewSmoothScrollingEntryListWidget.Precision.FLOAT_EPSILON))
-                scrolling.scrollTarget += (nearestRow - scrolling.scrollTarget) * Math.min(delta / 2.0, 1.0);
-            else
-                scrolling.scrollTarget = nearestRow;
-        }
-        scrolling.updatePosition(delta);
-    }
-    
-    public void updateSearch(String searchTerm) {
-        lastSearchArguments = SearchArgument.processSearchTerm(searchTerm);
-        Set<EntryStack> list = Sets.newLinkedHashSet();
-        for (EntryStack stack : EntryRegistry.getInstance().getStacksList()) {
-            if (canLastSearchTermsBeAppliedTo(stack)) {
-                list.add(stack.copy().setting(EntryStack.Settings.CHECK_AMOUNT, EntryStack.Settings.FALSE).setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE).setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE));
-            }
-        }
-        
-        entryStacks = Lists.newArrayList(list);
-        updateEntriesPosition();
-    }
-    
-    public boolean canLastSearchTermsBeAppliedTo(EntryStack stack) {
-        return lastSearchArguments.isEmpty() || SearchArgument.canSearchTermsBeAppliedTo(stack, lastSearchArguments);
-    }
-    
-    public void updateEntriesPosition() {
-        this.innerBounds = updateInnerBounds(getBounds());
-        int width = innerBounds.width / entrySize();
-        int pageHeight = innerBounds.height / entrySize();
-        int slotsToPrepare = Math.max(entryStacks.size() * 3, width * pageHeight * 3);
-        int currentX = 0;
-        int currentY = 0;
-        List<EntryListEntry> entries = Lists.newArrayList();
-        for (int i = 0; i < slotsToPrepare; i++) {
-            int xPos = currentX * entrySize() + innerBounds.x;
-            int yPos = currentY * entrySize() + innerBounds.y;
-            entries.add(new EntryListEntry(xPos, yPos));
-            currentX++;
-            if (currentX >= width) {
-                currentX = 0;
-                currentY++;
-            }
-        }
-        this.entries = entries;
-        this.elements = Lists.newArrayList(entries);
-        this.elements.add(searchField);
+        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);
     }
     
     @Override
     public List<? extends Element> children() {
-        return elements;
-    }
-    
-    @Override
-    public boolean mouseClicked(double double_1, double double_2, int int_1) {
-        if (scrolling.updateDraggingState(double_1, double_2, int_1))
-            return true;
-        
-        if (getBounds().contains(double_1, double_2)) {
-            if (searchField.mouseClicked(double_1, double_2, int_1)) {
-                this.selectionPoint = null;
-                this.secondPoint = null;
-                return true;
-            } else if (selectAllButton.mouseClicked(double_1, double_2, int_1)) {
-                return true;
-            } else if (selectNoneButton.mouseClicked(double_1, double_2, int_1)) {
-                return true;
-            } else if (hideButton.mouseClicked(double_1, double_2, int_1)) {
-                return true;
-            } else if (showButton.mouseClicked(double_1, double_2, int_1)) {
-                return true;
-            }
-            if (int_1 == 0) {
-                this.selectionPoint = new Point(double_1, double_2 + scrolling.scrollAmount);
-                this.secondPoint = null;
-                return true;
-            }
-        }
-        return false;
-    }
-    
-    @Override
-    public boolean mouseReleased(double mouseX, double mouseY, int button) {
-        if (selectionPoint != null && button == 0 && secondPoint == null) {
-            this.secondPoint = new Point(mouseX, mouseY + scrolling.scrollAmount);
-            if (secondPoint.equals(selectionPoint)) {
-                secondPoint.translate(1, 1);
-            }
-            return true;
-        }
-        return super.mouseReleased(mouseX, mouseY, button);
+        return children;
     }
     
     @Override
-    public boolean charTyped(char chr, int keyCode) {
-        for (Element element : children())
-            if (element.charTyped(chr, keyCode))
-                return true;
-        return super.charTyped(chr, keyCode);
-    }
-    
-    @Override
-    public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
-        for (Element element : children())
-            if (element.keyPressed(keyCode, scanCode, modifiers))
-                return true;
-        if (Screen.isSelectAll(keyCode)) {
-            this.selectionPoint = new Point(0, 0);
-            this.secondPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
-            return true;
-        }
-        return false;
-    }
-    
-    public void updateArea(@Nullable String searchTerm) {
-        if (searchTerm != null)
-            updateSearch(searchTerm);
-        else if (entryStacks == null)
-            updateSearch("");
-        else
-            updateEntriesPosition();
-    }
-    
-    @Override
-    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
-        if (getBounds().contains(double_1, double_2)) {
-            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
-            return true;
-        }
-        super.mouseScrolled(double_1, double_2, double_3);
-        return true;
-    }
-    
-    private class EntryListEntry extends EntryWidget {
-        private int backupY;
-        
-        private EntryListEntry(int x, int y) {
-            super(new Point(x, y));
-            this.backupY = y;
-            getBounds().width = getBounds().height = entrySize();
-            interactableFavorites(false);
-            interactable(false);
-            noHighlight();
-        }
-        
-        @Override
-        public boolean containsMouse(double mouseX, double mouseY) {
-            return super.containsMouse(mouseX, mouseY) && FilteringEntry.this.getBounds().contains(mouseX, mouseY);
-        }
-        
-        @Override
-        protected void drawHighlighted(MatrixStack matrices, int mouseX, int mouseY, float delta) {
-            
-        }
-        
-        @Override
-        public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
-            super.render(matrices, mouseX, mouseY, delta);
-            if (isSelected()) {
-                Rectangle bounds = getBounds();
-                RenderSystem.disableDepthTest();
-                fillGradient(matrices, bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), 0x896b70fa, 0x896b70fa);
-                RenderSystem.enableDepthTest();
-            }
-        }
-        
-        @Override
-        public EntryStack getCurrentEntry() {
-            return super.getCurrentEntry();
-        }
-        
-        public boolean isSelected() {
-            return getSelection().intersects(getBounds());
-        }
-        
-        public boolean isFiltered() {
-            return CollectionUtils.findFirstOrNullEqualsEntryIgnoreAmount(configFiltered, getCurrentEntry()) != null;
-        }
-        
-        @Override
-        protected void drawBackground(MatrixStack matrices, int mouseX, int mouseY, float delta) {
-            if (isFiltered()) {
-                Rectangle bounds = getBounds();
-                RenderSystem.disableDepthTest();
-                fillGradient(matrices, bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), 0xffff0000, 0xffff0000);
-                RenderSystem.enableDepthTest();
-            }
-        }
-        
-        @Override
-        protected void queueTooltip(MatrixStack matrices, int mouseX, int mouseY, float delta) {
-            if (searchField.containsMouse(mouseX, mouseY))
-                return;
-            Tooltip tooltip = getCurrentTooltip(new Point(mouseX, mouseY));
-            if (tooltip != null) {
-                FilteringEntry.this.tooltip = tooltip;
-            }
-        }
+    public boolean isEdited() {
+        return super.isEdited() || edited;
     }
 }

+ 510 - 0
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringScreen.java

@@ -0,0 +1,510 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config.entry;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.clothconfig2.api.ScrollingContainer;
+import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.math.impl.PointHelper;
+import me.shedaniel.rei.api.ConfigObject;
+import me.shedaniel.rei.api.EntryRegistry;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.widgets.Tooltip;
+import me.shedaniel.rei.gui.OverlaySearchField;
+import me.shedaniel.rei.gui.widget.EntryWidget;
+import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.impl.SearchArgument;
+import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.Screen;
+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.math.MathHelper;
+import net.minecraft.util.math.Matrix4f;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static me.shedaniel.rei.gui.widget.EntryListWidget.entrySize;
+
+@ApiStatus.Internal
+public class FilteringScreen extends Screen {
+    protected List<EntryStack> selected = Lists.newArrayList();
+    protected final ScrollingContainer scrolling = new ScrollingContainer() {
+        @Override
+        public int getMaxScrollHeight() {
+            return MathHelper.ceil(entryStacks.size() / (innerBounds.width / (float) entrySize())) * entrySize() + 28;
+        }
+        
+        @Override
+        public Rectangle getBounds() {
+            return FilteringScreen.this.getBounds();
+        }
+        
+        @Override
+        public int getScrollBarX() {
+            return width - 7;
+        }
+    };
+    
+    Screen parent;
+    private FilteringEntry filteringEntry;
+    private Tooltip tooltip = null;
+    private List<EntryStack> entryStacks = null;
+    private Rectangle innerBounds;
+    private List<EntryListEntry> entries = Collections.emptyList();
+    private List<Element> elements = Collections.emptyList();
+    
+    private Point selectionPoint = null;
+    private Point secondPoint = null;
+    
+    private OverlaySearchField searchField;
+    private ButtonWidget selectAllButton;
+    private ButtonWidget selectNoneButton;
+    private ButtonWidget hideButton;
+    private ButtonWidget showButton;
+    private ButtonWidget backButton;
+    
+    private List<SearchArgument.SearchArguments> lastSearchArguments = Collections.emptyList();
+    
+    public FilteringScreen(FilteringEntry filteringEntry) {
+        super(new TranslatableText("config.roughlyenoughitems.filteringScreen"));
+        this.filteringEntry = filteringEntry;
+        this.searchField = new OverlaySearchField(0, 0, 0, 0);
+        {
+            Text selectAllText = new TranslatableText("config.roughlyenoughitems.filteredEntries.selectAll");
+            this.selectAllButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(selectAllText) + 10, 20, selectAllText, button -> {
+                this.selectionPoint = new Point(-Integer.MAX_VALUE / 2, -Integer.MAX_VALUE / 2);
+                this.secondPoint = new Point(Integer.MAX_VALUE / 2, Integer.MAX_VALUE / 2);
+            });
+        }
+        {
+            Text selectNoneText = new TranslatableText("config.roughlyenoughitems.filteredEntries.selectNone");
+            this.selectNoneButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(selectNoneText) + 10, 20, selectNoneText, button -> {
+                this.selectionPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
+                this.secondPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
+            });
+        }
+        {
+            Text hideText = new TranslatableText("config.roughlyenoughitems.filteredEntries.hide");
+            this.hideButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(hideText) + 10, 20, hideText, button -> {
+                for (int i = 0; i < entryStacks.size(); i++) {
+                    EntryStack stack = entryStacks.get(i);
+                    EntryListEntry entry = entries.get(i);
+                    entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
+                    if (entry.isSelected() && !entry.isFiltered()) {
+                        filteringEntry.configFiltered.add(stack);
+                        filteringEntry.edited = true;
+                    }
+                }
+            });
+        }
+        {
+            Text showText = new TranslatableText("config.roughlyenoughitems.filteredEntries.show");
+            this.showButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(showText) + 10, 20, showText, button -> {
+                for (int i = 0; i < entryStacks.size(); i++) {
+                    EntryStack stack = entryStacks.get(i);
+                    EntryListEntry entry = entries.get(i);
+                    entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
+                    if (entry.isSelected() && filteringEntry.configFiltered.remove(stack)) {
+                        filteringEntry.edited = true;
+                    }
+                }
+            });
+        }
+        {
+            Text backText = new LiteralText("↩ ").append(new TranslatableText("gui.back"));
+            this.backButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(backText) + 10, 20, backText, button -> {
+                client.openScreen(parent);
+                this.parent = null;
+            });
+        }
+        this.searchField.isMain = false;
+    }
+    
+    private static Rectangle updateInnerBounds(Rectangle bounds) {
+        int width = Math.max(MathHelper.floor((bounds.width - 2 - 6) / (float) entrySize()), 1);
+        return new Rectangle((int) (bounds.getCenterX() - width * entrySize() / 2f), bounds.y + 5, width * entrySize(), bounds.height);
+    }
+    
+    public Rectangle getBounds() {
+        return new Rectangle(0, 30, width, this.height - 30);
+    }
+    
+    @Override
+    protected void init() {
+        super.init();
+        Rectangle bounds = getBounds();
+        updateSearch(this.searchField.getText());
+        this.searchField.getBounds().setBounds(bounds.getCenterX() - 75, bounds.getMaxY() - 22, 150, 18);
+        this.selectAllButton.x = 2;
+        this.selectAllButton.y = bounds.getMaxY() - 22;
+        this.selectNoneButton.x = 4 + selectAllButton.getWidth();
+        this.selectNoneButton.y = bounds.getMaxY() - 22;
+        this.hideButton.x = bounds.getMaxX() - hideButton.getWidth() - showButton.getWidth() - 4;
+        this.hideButton.y = bounds.getMaxY() - 22;
+        this.showButton.x = bounds.getMaxX() - showButton.getWidth() - 2;
+        this.showButton.y = bounds.getMaxY() - 22;
+        this.backButton.x = 4;
+        this.backButton.y = 4;
+        this.searchField.setChangedListener(this::updateSearch);
+    }
+    
+    protected void renderHoleBackground(MatrixStack matrices, int y1, int y2, int tint, int alpha1, int alpha2) {
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder buffer = tessellator.getBuffer();
+        this.client.getTextureManager().bindTexture(BACKGROUND_TEXTURE);
+        Matrix4f matrix = matrices.peek().getModel();
+        RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        float float_1 = 32.0F;
+        buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
+        buffer.vertex(matrix, 0, y2, 0.0F).texture(0.0F, y2 / 32.0F).color(tint, tint, tint, alpha2).next();
+        buffer.vertex(matrix, this.width, y2, 0.0F).texture(this.width / 32.0F, y2 / 32.0F).color(tint, tint, tint, alpha2).next();
+        buffer.vertex(matrix, this.width, y1, 0.0F).texture(this.width / 32.0F, y1 / 32.0F).color(tint, tint, tint, alpha1).next();
+        buffer.vertex(matrix, 0, y1, 0.0F).texture(0.0F, y1 / 32.0F).color(tint, tint, tint, alpha1).next();
+        tessellator.draw();
+    }
+    
+    @Override
+    public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+        renderHoleBackground(matrices, 0, height, 32, 255, 255);
+        Rectangle bounds = getBounds();
+        tooltip = null;
+        if (bounds.isEmpty())
+            return;
+        ScissorsHandler.INSTANCE.scissor(bounds);
+        for (EntryListEntry entry : entries)
+            entry.clearStacks();
+        int skip = Math.max(0, MathHelper.floor(scrolling.scrollAmount / (float) entrySize()));
+        int nextIndex = skip * innerBounds.width / entrySize();
+        int i = nextIndex;
+        for (; i < entryStacks.size(); i++) {
+            EntryStack stack = entryStacks.get(i);
+            EntryListEntry entry = entries.get(nextIndex);
+            entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
+            if (entry.getBounds().y > bounds.getMaxY())
+                break;
+            entry.entry(stack);
+            entry.render(matrices, mouseX, mouseY, delta);
+            nextIndex++;
+        }
+        updatePosition(delta);
+        scrolling.renderScrollBar(0xff000000, 1, REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
+        matrices.push();
+        matrices.translate(0, 0, 300);
+        this.searchField.laterRender(matrices, mouseX, mouseY, delta);
+        this.selectAllButton.render(matrices, mouseX, mouseY, delta);
+        this.selectNoneButton.render(matrices, mouseX, mouseY, delta);
+        this.hideButton.render(matrices, mouseX, mouseY, delta);
+        this.showButton.render(matrices, mouseX, mouseY, delta);
+        matrices.pop();
+        
+        ScissorsHandler.INSTANCE.removeLastScissor();
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder buffer = tessellator.getBuffer();
+        RenderSystem.enableBlend();
+        RenderSystem.blendFuncSeparate(770, 771, 0, 1);
+        RenderSystem.disableAlphaTest();
+        RenderSystem.shadeModel(7425);
+        RenderSystem.disableTexture();
+        Matrix4f matrix = matrices.peek().getModel();
+        buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
+        buffer.vertex(matrix, 0, bounds.y + 4, 0.0F).texture(0.0F, 1.0F).color(0, 0, 0, 0).next();
+        buffer.vertex(matrix, width, bounds.y + 4, 0.0F).texture(1.0F, 1.0F).color(0, 0, 0, 0).next();
+        buffer.vertex(matrix, width, bounds.y, 0.0F).texture(1.0F, 0.0F).color(0, 0, 0, 255).next();
+        buffer.vertex(matrix, 0, bounds.y, 0.0F).texture(0.0F, 0.0F).color(0, 0, 0, 255).next();
+        tessellator.draw();
+        RenderSystem.enableTexture();
+        RenderSystem.shadeModel(7424);
+        RenderSystem.enableAlphaTest();
+        RenderSystem.disableBlend();
+        renderHoleBackground(matrices, 0, bounds.y, 64, 255, 255);
+        
+        this.backButton.render(matrices, mouseX, mouseY, delta);
+        
+        if (tooltip != null) {
+            ScreenHelper.getLastOverlay().renderTooltip(matrices, tooltip);
+        }
+        
+        this.textRenderer.drawWithShadow(matrices, this.title, this.width / 2.0F - this.textRenderer.getWidth(this.title) / 2.0F, 12.0F, -1);
+    }
+    
+    private Rectangle getSelection() {
+        if (selectionPoint != null) {
+            Point p = secondPoint;
+            if (p == null) {
+                p = PointHelper.ofMouse();
+                p.translate(0, (int) scrolling.scrollAmount);
+            }
+            int left = Math.min(p.x, selectionPoint.x);
+            int top = Math.min(p.y, selectionPoint.y);
+            int right = Math.max(p.x, selectionPoint.x);
+            int bottom = Math.max(p.y, selectionPoint.y);
+            return new Rectangle(left, (int) (top - scrolling.scrollAmount), right - left, bottom - top);
+        }
+        return new Rectangle(0, 0, 0, 0);
+    }
+    
+    @Override
+    public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
+        if (scrolling.mouseDragged(mouseX, mouseY, button, dx, dy, ConfigObject.getInstance().doesSnapToRows(), entrySize()))
+            return true;
+        return super.mouseDragged(mouseX, mouseY, button, dx, dy);
+    }
+    
+    private void updatePosition(float delta) {
+        if (ConfigObject.getInstance().doesSnapToRows() && scrolling.scrollTarget >= 0 && scrolling.scrollTarget <= scrolling.getMaxScroll()) {
+            double nearestRow = Math.round(scrolling.scrollTarget / (double) entrySize()) * (double) entrySize();
+            if (!DynamicNewSmoothScrollingEntryListWidget.Precision.almostEquals(scrolling.scrollTarget, nearestRow, DynamicNewSmoothScrollingEntryListWidget.Precision.FLOAT_EPSILON))
+                scrolling.scrollTarget += (nearestRow - scrolling.scrollTarget) * Math.min(delta / 2.0, 1.0);
+            else
+                scrolling.scrollTarget = nearestRow;
+        }
+        scrolling.updatePosition(delta);
+    }
+    
+    public void updateSearch(String searchTerm) {
+        lastSearchArguments = SearchArgument.processSearchTerm(searchTerm);
+        Set<EntryStack> list = Sets.newLinkedHashSet();
+        for (EntryStack stack : EntryRegistry.getInstance().getStacksList()) {
+            if (canLastSearchTermsBeAppliedTo(stack)) {
+                list.add(stack.copy().setting(EntryStack.Settings.CHECK_AMOUNT, EntryStack.Settings.FALSE).setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE).setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE));
+            }
+        }
+        
+        entryStacks = Lists.newArrayList(list);
+        updateEntriesPosition();
+    }
+    
+    public boolean canLastSearchTermsBeAppliedTo(EntryStack stack) {
+        return lastSearchArguments.isEmpty() || SearchArgument.canSearchTermsBeAppliedTo(stack, lastSearchArguments);
+    }
+    
+    public void updateEntriesPosition() {
+        this.innerBounds = updateInnerBounds(getBounds());
+        int width = innerBounds.width / entrySize();
+        int pageHeight = innerBounds.height / entrySize();
+        int slotsToPrepare = Math.max(entryStacks.size() * 3, width * pageHeight * 3);
+        int currentX = 0;
+        int currentY = 0;
+        List<EntryListEntry> entries = Lists.newArrayList();
+        for (int i = 0; i < slotsToPrepare; i++) {
+            int xPos = currentX * entrySize() + innerBounds.x;
+            int yPos = currentY * entrySize() + innerBounds.y;
+            entries.add(new EntryListEntry(xPos, yPos));
+            currentX++;
+            if (currentX >= width) {
+                currentX = 0;
+                currentY++;
+            }
+        }
+        this.entries = entries;
+        this.elements = Lists.newArrayList(entries);
+        this.elements.add(searchField);
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return elements;
+    }
+    
+    @Override
+    public boolean mouseClicked(double double_1, double double_2, int int_1) {
+        if (scrolling.updateDraggingState(double_1, double_2, int_1))
+            return true;
+        
+        if (getBounds().contains(double_1, double_2)) {
+            if (searchField.mouseClicked(double_1, double_2, int_1)) {
+                this.selectionPoint = null;
+                this.secondPoint = null;
+                return true;
+            } else if (selectAllButton.mouseClicked(double_1, double_2, int_1)) {
+                return true;
+            } else if (selectNoneButton.mouseClicked(double_1, double_2, int_1)) {
+                return true;
+            } else if (hideButton.mouseClicked(double_1, double_2, int_1)) {
+                return true;
+            } else if (showButton.mouseClicked(double_1, double_2, int_1)) {
+                return true;
+            }
+            if (int_1 == 0) {
+                this.selectionPoint = new Point(double_1, double_2 + scrolling.scrollAmount);
+                this.secondPoint = null;
+                return true;
+            }
+        }
+        if (backButton.mouseClicked(double_1, double_2, int_1)) {
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public boolean mouseReleased(double mouseX, double mouseY, int button) {
+        if (selectionPoint != null && button == 0 && secondPoint == null) {
+            this.secondPoint = new Point(mouseX, mouseY + scrolling.scrollAmount);
+            if (secondPoint.equals(selectionPoint)) {
+                secondPoint.translate(1, 1);
+            }
+            return true;
+        }
+        return super.mouseReleased(mouseX, mouseY, button);
+    }
+    
+    @Override
+    public boolean charTyped(char chr, int keyCode) {
+        for (Element element : children())
+            if (element.charTyped(chr, keyCode))
+                return true;
+        return super.charTyped(chr, keyCode);
+    }
+    
+    @Override
+    public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+        for (Element element : children())
+            if (element.keyPressed(keyCode, scanCode, modifiers))
+                return true;
+        if (Screen.isSelectAll(keyCode)) {
+            this.selectionPoint = new Point(0, 0);
+            this.secondPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
+            return true;
+        }
+        if (keyCode == 256 && this.shouldCloseOnEsc()) {
+            this.backButton.onPress();
+            return true;
+        } else if (keyCode == 258) {
+            boolean bl = !hasShiftDown();
+            if (!this.changeFocus(bl)) {
+                this.changeFocus(bl);
+            }
+        
+            return true;
+        }
+        return false;
+    }
+    
+    public void updateArea(@Nullable String searchTerm) {
+        if (searchTerm != null)
+            updateSearch(searchTerm);
+        else if (entryStacks == null)
+            updateSearch("");
+        else
+            updateEntriesPosition();
+    }
+    
+    @Override
+    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        if (getBounds().contains(double_1, double_2)) {
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+            return true;
+        }
+        super.mouseScrolled(double_1, double_2, double_3);
+        return true;
+    }
+    
+    private class EntryListEntry extends EntryWidget {
+        private int backupY;
+        
+        private EntryListEntry(int x, int y) {
+            super(new Point(x, y));
+            this.backupY = y;
+            getBounds().width = getBounds().height = entrySize();
+            interactableFavorites(false);
+            interactable(false);
+            noHighlight();
+        }
+        
+        @Override
+        public boolean containsMouse(double mouseX, double mouseY) {
+            return super.containsMouse(mouseX, mouseY) && FilteringScreen.this.getBounds().contains(mouseX, mouseY);
+        }
+        
+        @Override
+        protected void drawHighlighted(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+            
+        }
+        
+        @Override
+        public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+            super.render(matrices, mouseX, mouseY, delta);
+            if (isSelected()) {
+                Rectangle bounds = getBounds();
+                RenderSystem.disableDepthTest();
+                fillGradient(matrices, bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), 0x896b70fa, 0x896b70fa);
+                RenderSystem.enableDepthTest();
+            }
+        }
+        
+        @Override
+        public EntryStack getCurrentEntry() {
+            return super.getCurrentEntry();
+        }
+        
+        public boolean isSelected() {
+            return getSelection().intersects(getBounds());
+        }
+        
+        public boolean isFiltered() {
+            return CollectionUtils.findFirstOrNullEqualsEntryIgnoreAmount(filteringEntry.configFiltered, getCurrentEntry()) != null;
+        }
+        
+        @Override
+        protected void drawBackground(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+            if (isFiltered()) {
+                Rectangle bounds = getBounds();
+                RenderSystem.disableDepthTest();
+                fillGradient(matrices, bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), 0xffff0000, 0xffff0000);
+                RenderSystem.enableDepthTest();
+            }
+        }
+        
+        @Override
+        protected void queueTooltip(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+            if (searchField.containsMouse(mouseX, mouseY))
+                return;
+            Tooltip tooltip = getCurrentTooltip(new Point(mouseX, mouseY));
+            if (tooltip != null) {
+                FilteringScreen.this.tooltip = tooltip;
+            }
+        }
+    }
+}

+ 17 - 12
src/main/java/me/shedaniel/rei/gui/config/entry/NoFilteringEntry.java

@@ -23,15 +23,18 @@
 
 package me.shedaniel.rei.gui.config.entry;
 
+import com.google.common.collect.ImmutableList;
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
-import me.shedaniel.clothconfig2.gui.ClothConfigScreen;
-import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
 import me.shedaniel.rei.api.EntryStack;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.AbstractButtonWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.util.NarratorManager;
+import net.minecraft.client.util.Window;
 import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.TranslatableText;
 import org.jetbrains.annotations.ApiStatus;
 
 import java.util.Collections;
@@ -41,12 +44,16 @@ import java.util.function.Consumer;
 
 @ApiStatus.Internal
 public class NoFilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
+    private int width;
     private Consumer<List<EntryStack>> saveConsumer;
     private List<EntryStack> defaultValue;
     private List<EntryStack> configFiltered;
+    private final AbstractButtonWidget buttonWidget = new ButtonWidget(0, 0, 0, 20, new TranslatableText("config.roughlyenoughitems.filteredEntries.loadWorldFirst"), button -> {});
+    private final List<Element> children = ImmutableList.of(buttonWidget);
     
-    public NoFilteringEntry(List<EntryStack> configFiltered, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer) {
+    public NoFilteringEntry(int width,List<EntryStack> configFiltered, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer) {
         super(NarratorManager.EMPTY, false);
+        this.width = width;
         this.configFiltered = configFiltered;
         this.defaultValue = defaultValue;
         this.saveConsumer = saveConsumer;
@@ -67,20 +74,18 @@ public class NoFilteringEntry extends AbstractConfigListEntry<List<EntryStack>>
         saveConsumer.accept(getValue());
     }
     
-    @SuppressWarnings("rawtypes")
     @Override
     public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
-        DynamicEntryListWidget parent = getParent();
-        drawCenteredString(matrices, MinecraftClient.getInstance().textRenderer, I18n.translate("config.roughlyenoughitems.filteredEntries.loadWorldFirst"), (parent.right - parent.left) / 2 + parent.left, (parent.bottom - parent.top) / 2 + parent.top - 5, -1);
-    }
-    
-    @Override
-    public boolean mouseScrolled(double d, double e, double amount) {
-        return true;
+        Window window = MinecraftClient.getInstance().getWindow();
+        this.buttonWidget.active = false;
+        this.buttonWidget.y = y;
+        this.buttonWidget.x = x + entryWidth / 2 - width / 2;
+        this.buttonWidget.setWidth(width);
+        this.buttonWidget.render(matrices, mouseX, mouseY, delta);
     }
     
     @Override
     public List<? extends Element> children() {
-        return Collections.emptyList();
+        return children;
     }
 }

+ 4 - 4
src/main/java/me/shedaniel/rei/gui/config/entry/RecipeScreenTypeEntry.java

@@ -47,11 +47,11 @@ public class RecipeScreenTypeEntry extends TooltipListEntry<RecipeScreenType> {
     private RecipeScreenType type;
     private RecipeScreenType defaultValue;
     private Consumer<RecipeScreenType> save;
-    private AbstractButtonWidget buttonWidget = new AbstractPressableButtonWidget(0, 0, 0, 20, NarratorManager.EMPTY) {
+    private final AbstractButtonWidget buttonWidget = new AbstractPressableButtonWidget(0, 0, 0, 20, NarratorManager.EMPTY) {
         @Override
         public void onPress() {
-            MinecraftClient.getInstance().openScreen(new PreRecipeViewingScreen(getScreen(), type, false, original -> {
-                MinecraftClient.getInstance().openScreen(getScreen());
+            MinecraftClient.getInstance().openScreen(new PreRecipeViewingScreen(getConfigScreen(), type, false, original -> {
+                MinecraftClient.getInstance().openScreen(getConfigScreen());
                 type = original ? RecipeScreenType.ORIGINAL : RecipeScreenType.VILLAGER;
             }));
         }
@@ -62,7 +62,7 @@ public class RecipeScreenTypeEntry extends TooltipListEntry<RecipeScreenType> {
             super.render(matrices, mouseX, mouseY, delta);
         }
     };
-    private List<Element> children = ImmutableList.of(buttonWidget);
+    private final List<Element> children = ImmutableList.of(buttonWidget);
     
     @SuppressWarnings("deprecation")
     public RecipeScreenTypeEntry(int width, Text fieldName, RecipeScreenType type, RecipeScreenType defaultValue, Consumer<RecipeScreenType> save) {

+ 94 - 0
src/main/java/me/shedaniel/rei/gui/config/entry/ReloadPluginsEntry.java

@@ -0,0 +1,94 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config.entry;
+
+import com.google.common.collect.ImmutableList;
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.RecipeHelper;
+import me.shedaniel.rei.gui.ConfigReloadingScreen;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.widget.AbstractButtonWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.util.NarratorManager;
+import net.minecraft.client.util.Window;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.TranslatableText;
+import net.minecraft.util.Unit;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.List;
+import java.util.Optional;
+
+@ApiStatus.Internal
+public class ReloadPluginsEntry extends AbstractConfigListEntry<Unit> {
+    private int width;
+    private AbstractButtonWidget buttonWidget = new ButtonWidget(0, 0, 0, 20, NarratorManager.EMPTY, button -> RoughlyEnoughItemsCore.syncRecipes(null)) {
+        @Override
+        public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+            if (RecipeHelper.getInstance().arePluginsLoading()) {
+                MinecraftClient.getInstance().openScreen(new ConfigReloadingScreen(MinecraftClient.getInstance().currentScreen));
+            } else
+                super.render(matrices, mouseX, mouseY, delta);
+        }
+    };
+    private List<Element> children = ImmutableList.of(buttonWidget);
+    
+    public ReloadPluginsEntry(int width) {
+        super(NarratorManager.EMPTY, false);
+        this.width = width;
+        buttonWidget.setMessage(new TranslatableText("text.rei.reload_config"));
+    }
+    
+    @Override
+    public Unit getValue() {
+        return Unit.INSTANCE;
+    }
+    
+    @Override
+    public Optional<Unit> getDefaultValue() {
+        return Optional.of(Unit.INSTANCE);
+    }
+    
+    @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 isSelected, float 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);
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return children;
+    }
+}

+ 3 - 3
src/main/java/me/shedaniel/rei/gui/credits/CreditsEntryListWidget.java

@@ -24,10 +24,10 @@
 package me.shedaniel.rei.gui.credits;
 
 import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
-import net.minecraft.class_5348;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.DrawableHelper;
 import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.StringRenderable;
 import net.minecraft.text.Text;
 import org.jetbrains.annotations.ApiStatus;
 
@@ -109,7 +109,7 @@ public class CreditsEntryListWidget extends DynamicNewSmoothScrollingEntryListWi
     
     public static class TranslationCreditsItem extends CreditsItem {
         private Text language;
-        private List<class_5348> translators;
+        private List<StringRenderable> translators;
         private int maxWidth;
         
         public TranslationCreditsItem(Text language, Text translators, int width, int maxWidth) {
@@ -122,7 +122,7 @@ public class CreditsEntryListWidget extends DynamicNewSmoothScrollingEntryListWi
         public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
             MinecraftClient.getInstance().textRenderer.drawWithShadow(matrices, language, x + 5, y + 5, -1);
             int yy = y + 5;
-            for (class_5348 translator : translators) {
+            for (StringRenderable translator : translators) {
                 MinecraftClient.getInstance().textRenderer.drawWithShadow(matrices, translator, x + 5 + maxWidth, yy, -1);
                 yy += 12;
             }

+ 4 - 5
src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java

@@ -34,9 +34,8 @@ import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.api.widgets.Tooltip;
-import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
-import me.shedaniel.rei.gui.config.ItemListOrdering;
+import me.shedaniel.rei.gui.config.EntryPanelOrdering;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.impl.SearchArgument;
 import me.shedaniel.rei.utils.CollectionUtils;
@@ -519,10 +518,10 @@ public class EntryListWidget extends WidgetWithBounds {
                     }
                 }
             }
-            ItemListOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
-            if (ordering == ItemListOrdering.name)
+            EntryPanelOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
+            if (ordering == EntryPanelOrdering.NAME)
                 list.sort(ENTRY_NAME_COMPARER);
-            if (ordering == ItemListOrdering.item_groups)
+            if (ordering == EntryPanelOrdering.GROUPS)
                 list.sort(ENTRY_GROUP_COMPARER);
             if (!ConfigObject.getInstance().isItemListAscending())
                 Collections.reverse(list);

+ 7 - 7
src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java

@@ -34,7 +34,7 @@ import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.api.widgets.Tooltip;
-import me.shedaniel.rei.gui.config.ItemListOrdering;
+import me.shedaniel.rei.gui.config.EntryPanelOrdering;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
@@ -183,10 +183,10 @@ public class FavoritesListWidget extends WidgetWithBounds {
                         list.add(stack.copy().setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE).setting(EntryStack.Settings.Item.RENDER_ENCHANTMENT_GLINT, RENDER_ENCHANTMENT_GLINT));
                     }
                 }
-                ItemListOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
-                if (ordering == ItemListOrdering.name)
+                EntryPanelOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
+                if (ordering == EntryPanelOrdering.NAME)
                     list.sort(ENTRY_NAME_COMPARER);
-                if (ordering == ItemListOrdering.item_groups)
+                if (ordering == EntryPanelOrdering.GROUPS)
                     list.sort(ENTRY_GROUP_COMPARER);
                 if (!ConfigObject.getInstance().isItemListAscending())
                     Collections.reverse(list);
@@ -200,10 +200,10 @@ public class FavoritesListWidget extends WidgetWithBounds {
                         continue;
                     list.add(stack.copy().setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE).setting(EntryStack.Settings.Item.RENDER_ENCHANTMENT_GLINT, RENDER_ENCHANTMENT_GLINT));
                 }
-                ItemListOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
-                if (ordering == ItemListOrdering.name)
+                EntryPanelOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
+                if (ordering == EntryPanelOrdering.NAME)
                     list.sort(ENTRY_NAME_COMPARER);
-                if (ordering == ItemListOrdering.item_groups)
+                if (ordering == EntryPanelOrdering.GROUPS)
                     list.sort(ENTRY_GROUP_COMPARER);
                 if (!ConfigObject.getInstance().isItemListAscending())
                     Collections.reverse(list);

+ 10 - 19
src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java

@@ -35,25 +35,24 @@ import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.Jankson;
 import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.JsonObject;
 import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.JsonPrimitive;
 import me.sargunvohra.mcmods.autoconfig1u.util.Utils;
-import me.shedaniel.cloth.hooks.ScreenHooks;
+import me.shedaniel.cloth.api.client.events.v0.ScreenHooks;
 import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
 import me.shedaniel.clothconfig2.api.Modifier;
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
-import me.shedaniel.rei.gui.ConfigReloadingScreen;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.config.RecipeScreenType;
 import me.shedaniel.rei.gui.config.entry.FilteringEntry;
 import me.shedaniel.rei.gui.config.entry.NoFilteringEntry;
 import me.shedaniel.rei.gui.config.entry.RecipeScreenTypeEntry;
+import me.shedaniel.rei.gui.config.entry.ReloadPluginsEntry;
 import me.shedaniel.rei.gui.credits.CreditsScreen;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.widget.ButtonWidget;
 import net.minecraft.client.util.InputUtil;
-import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.text.LiteralText;
 import net.minecraft.text.TranslatableText;
 import net.minecraft.util.math.MathHelper;
@@ -112,9 +111,9 @@ public class ConfigManagerImpl implements ConfigManager {
                 , (field) -> field.getType() == RecipeScreenType.class, ConfigObjectImpl.UseSpecialRecipeTypeScreen.class);
         guiRegistry.registerAnnotationProvider((i13n, field, config, defaults, guiProvider) ->
                         REIHelper.getInstance().getPreviousContainerScreen() == null || MinecraftClient.getInstance().getNetworkHandler() == null || MinecraftClient.getInstance().getNetworkHandler().getRecipeManager() == null ?
-                                Collections.singletonList(new NoFilteringEntry(getUnsafely(field, config, new ArrayList<>()), getUnsafely(field, defaults), list -> setUnsafely(field, config, list)))
+                                Collections.singletonList(new NoFilteringEntry(220, getUnsafely(field, config, new ArrayList<>()), getUnsafely(field, defaults), list -> setUnsafely(field, config, list)))
                                 :
-                                Collections.singletonList(new FilteringEntry(getUnsafely(field, config, new ArrayList<>()), getUnsafely(field, defaults), list -> setUnsafely(field, config, list)))
+                                Collections.singletonList(new FilteringEntry(220, getUnsafely(field, config, new ArrayList<>()), getUnsafely(field, defaults), list -> setUnsafely(field, config, list)))
                 , (field) -> field.getType() == List.class, ConfigObjectImpl.UseFilteringScreen.class);
         saveConfig();
         RoughlyEnoughItemsCore.LOGGER.info("Config loaded.");
@@ -156,21 +155,13 @@ public class ConfigManagerImpl implements ConfigManager {
             provider.setOptionFunction((baseI13n, field) -> field.isAnnotationPresent(ConfigObjectImpl.DontApplyFieldName.class) ? baseI13n : String.format("%s.%s", baseI13n, field.getName()));
             provider.setCategoryFunction((baseI13n, categoryName) -> String.format("%s.%s", baseI13n, categoryName));
             provider.setBuildFunction(builder -> {
+                builder.setGlobalized(true);
+                builder.setGlobalizedExpanded(true);
+                if (MinecraftClient.getInstance().getNetworkHandler() != null && MinecraftClient.getInstance().getNetworkHandler().getRecipeManager() != null) {
+                    builder.getOrCreateCategory(new TranslatableText("config.roughlyenoughitems.advanced")).getEntries().add(0, new ReloadPluginsEntry(220));
+                }
                 return builder.setAfterInitConsumer(screen -> {
-                    if (MinecraftClient.getInstance().getNetworkHandler() != null && MinecraftClient.getInstance().getNetworkHandler().getRecipeManager() != null) {
-                        ((ScreenHooks) screen).cloth_addButton(new net.minecraft.client.gui.widget.ButtonWidget(4, 4, 100, 20, new TranslatableText("text.rei.reload_config"), buttonWidget -> {
-                            RoughlyEnoughItemsCore.syncRecipes(null);
-                        }) {
-                            @Override
-                            public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
-                                if (RecipeHelper.getInstance().arePluginsLoading()) {
-                                    MinecraftClient.getInstance().openScreen(new ConfigReloadingScreen(MinecraftClient.getInstance().currentScreen));
-                                } else
-                                    super.render(matrices, mouseX, mouseY, delta);
-                            }
-                        });
-                    }
-                    ((ScreenHooks) screen).cloth_addButton(new ButtonWidget(screen.width - 104, 4, 100, 20, new TranslatableText("text.rei.credits"), button -> {
+                    ((ScreenHooks) screen).cloth$addButtonWidget(new ButtonWidget(screen.width - 104, 4, 100, 20, new TranslatableText("text.rei.credits"), button -> {
                         MinecraftClient.getInstance().openScreen(new CreditsScreen(screen));
                     }));
                 }).setSavingRunnable(() -> {

+ 148 - 123
src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java

@@ -46,64 +46,58 @@ import java.util.List;
 @Config(name = "roughlyenoughitems/config")
 public class ConfigObjectImpl implements ConfigObject, ConfigData {
     
-    @ConfigEntry.Category("!general") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
-    public General general = new General();
+    @ConfigEntry.Category("basics") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
+    public Basics basics = new Basics();
     @ConfigEntry.Category("appearance") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
     private Appearance appearance = new Appearance();
-    @ConfigEntry.Category("modules") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
-    private Modules modules = new Modules();
-    @ConfigEntry.Category("technical") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
-    private Technical technical = new Technical();
-    @ConfigEntry.Category("performance") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
-    private Performance performance = new Performance();
-    @ConfigEntry.Category("filtering") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
-    private Filtering filtering = new Filtering();
-    @ConfigEntry.Category("experimental") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName @ApiStatus.Experimental
-    private Experimental experimental = new Experimental();
+    @ConfigEntry.Category("functionality") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
+    private Functionality functionality = new Functionality();
+    @ConfigEntry.Category("advanced") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
+    private Advanced advanced = new Advanced();
     
     @Override
     public boolean isOverlayVisible() {
-        return general.overlayVisible;
+        return basics.overlayVisible;
     }
     
     @Override
     public void setOverlayVisible(boolean overlayVisible) {
-        general.overlayVisible = overlayVisible;
+        basics.overlayVisible = overlayVisible;
     }
     
     @Override
     public boolean isCheating() {
-        return general.cheating;
+        return basics.cheating;
     }
     
     @Override
     public void setCheating(boolean cheating) {
-        general.cheating = cheating;
+        basics.cheating = cheating;
     }
     
     @Override
-    public ItemListOrdering getItemListOrdering() {
-        return appearance.itemListOrdering.getOrdering();
+    public EntryPanelOrdering getItemListOrdering() {
+        return advanced.layout.entryPanelOrdering.getOrdering();
     }
     
     @Override
     public boolean isItemListAscending() {
-        return appearance.itemListOrdering.isAscending();
+        return advanced.layout.entryPanelOrdering.isAscending();
     }
     
     @Override
     public boolean isUsingDarkTheme() {
-        return appearance.darkTheme;
+        return appearance.theme == AppearanceTheme.DARK;
     }
     
     @Override
     public boolean isToastDisplayedOnCopyIdentifier() {
-        return modules.toastDisplayedOnCopyIdentifier;
+        return advanced.accessibility.toastDisplayedOnCopyIdentifier;
     }
     
     @Override
     public boolean doesRenderEntryEnchantmentGlint() {
-        return performance.renderEntryEnchantmentGlint;
+        return advanced.miscellaneous.renderEntryEnchantmentGlint;
     }
     
     @Override
@@ -113,7 +107,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     
     @Override
     public boolean shouldAppendModNames() {
-        return appearance.appendModNames;
+        return advanced.tooltips.appendModNames;
     }
     
     @Override
@@ -128,62 +122,62 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     
     @Override
     public boolean isLoadingDefaultPlugin() {
-        return technical.loadDefaultPlugin;
+        return advanced.miscellaneous.loadDefaultPlugin;
     }
     
     @Override
     public SearchFieldLocation getSearchFieldLocation() {
-        return appearance.searchFieldLocation;
+        return appearance.layout.searchFieldLocation;
     }
     
     @Override
     public boolean isLeftHandSidePanel() {
-        return appearance.mirrorItemPanel;
+        return advanced.accessibility.displayPanelLocation == DisplayPanelLocation.LEFT;
     }
     
     @Override
     public boolean isCraftableFilterEnabled() {
-        return modules.enableCraftableOnlyButton;
+        return appearance.layout.enableCraftableOnlyButton;
     }
     
     @Override
     public String getGamemodeCommand() {
-        return technical.gamemodeCommand;
+        return advanced.commands.gamemodeCommand;
     }
     
     @Override
     public String getGiveCommand() {
-        return technical.giveCommand;
+        return advanced.commands.giveCommand;
     }
     
     @Override
     public String getWeatherCommand() {
-        return technical.weatherCommand;
+        return advanced.commands.weatherCommand;
     }
     
     @Override
     public int getMaxRecipePerPage() {
-        return appearance.maxRecipesPerPage;
+        return advanced.layout.maxRecipesPerPage;
     }
     
     @Override
     public boolean doesShowUtilsButtons() {
-        return modules.showUtilsButtons;
+        return appearance.layout.showUtilsButtons;
     }
     
     @Override
     public boolean doesDisableRecipeBook() {
-        return modules.disableRecipeBook;
+        return functionality.disableRecipeBook;
     }
     
     @Override
     public boolean doesFixTabCloseContainer() {
-        return modules.fixTabCloseContainer;
+        return functionality.disableRecipeBook;
     }
     
     @Override
     public boolean areClickableRecipeArrowsEnabled() {
-        return appearance.clickableRecipeArrows;
+        return advanced.miscellaneous.clickableRecipeArrows;
     }
     
     @Override
@@ -193,147 +187,141 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     
     @Override
     public boolean doesVillagerScreenHavePermanentScrollBar() {
-        return appearance.villagerScreenPermanentScrollBar;
+        return advanced.accessibility.villagerScreenPermanentScrollBar;
     }
     
     @Override
     public boolean doesRegisterRecipesInAnotherThread() {
-        return technical.registerRecipesInAnotherThread;
+        return advanced.miscellaneous.registerRecipesInAnotherThread;
     }
     
     @Override
     public boolean doesSnapToRows() {
-        return appearance.snapToRows;
+        return advanced.accessibility.snapToRows;
     }
     
     @Override
     public boolean isFavoritesEnabled() {
-        return general.favoritesEnabled;
+        return basics.favoritesEnabled;
     }
     
     @Override
     public boolean doDisplayFavoritesTooltip() {
-        return isFavoritesEnabled() && appearance.displayFavoritesTooltip;
+        return isFavoritesEnabled() && advanced.tooltips.displayFavoritesTooltip;
     }
     
     @Override
     public boolean doesFastEntryRendering() {
-        return performance.newFastEntryRendering;
+        return advanced.miscellaneous.newFastEntryRendering;
     }
     
     @Override
     public boolean doDebugRenderTimeRequired() {
-        return technical.debugRenderTimeRequired;
+        return advanced.layout.debugRenderTimeRequired;
     }
     
     @Override
     public boolean doSearchFavorites() {
-        return appearance.searchFavorites;
+        return advanced.search.searchFavorites;
     }
     
     @Override
     public ModifierKeyCode getFavoriteKeyCode() {
-        return general.favoriteKeybind == null ? ModifierKeyCode.unknown() : general.favoriteKeybind;
+        return basics.keyBindings.favoriteKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.favoriteKeybind;
     }
     
     @Override
     public ModifierKeyCode getRecipeKeybind() {
-        return general.recipeKeybind == null ? ModifierKeyCode.unknown() : general.recipeKeybind;
+        return basics.keyBindings.recipeKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.recipeKeybind;
     }
     
     @Override
     public ModifierKeyCode getUsageKeybind() {
-        return general.usageKeybind == null ? ModifierKeyCode.unknown() : general.usageKeybind;
+        return basics.keyBindings.usageKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.usageKeybind;
     }
     
     @Override
     public ModifierKeyCode getHideKeybind() {
-        return general.hideKeybind == null ? ModifierKeyCode.unknown() : general.hideKeybind;
+        return basics.keyBindings.hideKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.hideKeybind;
     }
     
     @Override
     public ModifierKeyCode getPreviousPageKeybind() {
-        return general.previousPageKeybind == null ? ModifierKeyCode.unknown() : general.previousPageKeybind;
+        return basics.keyBindings.previousPageKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.previousPageKeybind;
     }
     
     @Override
     public ModifierKeyCode getNextPageKeybind() {
-        return general.nextPageKeybind == null ? ModifierKeyCode.unknown() : general.nextPageKeybind;
+        return basics.keyBindings.nextPageKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.nextPageKeybind;
     }
     
     @Override
     public ModifierKeyCode getFocusSearchFieldKeybind() {
-        return general.focusSearchFieldKeybind == null ? ModifierKeyCode.unknown() : general.focusSearchFieldKeybind;
+        return basics.keyBindings.focusSearchFieldKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.focusSearchFieldKeybind;
     }
     
     @Override
     public ModifierKeyCode getCopyRecipeIdentifierKeybind() {
-        return general.copyRecipeIdentifierKeybind == null ? ModifierKeyCode.unknown() : general.copyRecipeIdentifierKeybind;
+        return basics.keyBindings.copyRecipeIdentifierKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.copyRecipeIdentifierKeybind;
     }
     
     @Override
     public ModifierKeyCode getExportImageKeybind() {
-        return general.exportImageKeybind == null ? ModifierKeyCode.unknown() : general.exportImageKeybind;
+        return basics.keyBindings.exportImageKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.exportImageKeybind;
     }
     
     @Override
     public double getEntrySize() {
-        return appearance.entrySize;
-    }
-    
-    @Deprecated
-    @Override
-    public General getGeneral() {
-        return general;
+        return advanced.accessibility.entrySize;
     }
     
     @Override
     public boolean isUsingCompactTabs() {
-        return appearance.useCompactTabs;
+        return advanced.accessibility.useCompactTabs;
     }
     
     @Override
     public boolean isLowerConfigButton() {
-        return appearance.lowerConfigButton;
+        return appearance.layout.configButtonLocation == ConfigButtonPosition.LOWER;
     }
     
     @Override
     public List<EntryStack> getFavorites() {
-        return general.favorites;
+        return basics.favorites;
     }
     
     @Override
     public List<EntryStack> getFilteredStacks() {
-        return filtering.filteredStacks;
+        return advanced.filtering.filteredStacks;
     }
     
     @Override
     @ApiStatus.Experimental
     public boolean shouldAsyncSearch() {
-        return performance.asyncSearch;
+        return advanced.search.asyncSearch;
     }
     
     @Override
     @ApiStatus.Experimental
     public int getNumberAsyncSearch() {
-        return performance.numberAsyncSearch;
+        return advanced.search.numberAsyncSearch;
     }
     
     @Override
     @ApiStatus.Experimental
     public boolean doDebugSearchTimeRequired() {
-        return technical.debugSearchTimeRequired;
+        return advanced.search.debugSearchTimeRequired;
     }
     
     @Override
     @ApiStatus.Experimental
     public boolean isSubsetsEnabled() {
-        return experimental.isSubsetsEnabled;
+        return functionality.isSubsetsEnabled;
     }
     
     @Override
     public boolean shouldResizeDynamically() {
-        return appearance.resizeDynamically;
+        return advanced.accessibility.resizeDynamically;
     }
     
     @Retention(RetentionPolicy.RUNTIME)
@@ -356,11 +344,16 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         double max();
     }
     
-    public static class General {
+    public static class Basics {
         @ConfigEntry.Gui.Excluded public List<EntryStack> favorites = new ArrayList<>();
         @Comment("Declares whether cheating mode is on.") private boolean cheating = false;
-        @Comment("Declares whether REI is visible.") @ConfigEntry.Gui.Excluded private boolean overlayVisible = true;
         private boolean favoritesEnabled = true;
+        @ConfigEntry.Gui.CollapsibleObject(startExpanded = true)
+        private KeyBindings keyBindings = new KeyBindings();
+        @Comment("Declares whether REI is visible.") @ConfigEntry.Gui.Excluded private boolean overlayVisible = true;
+    }
+    
+    public static class KeyBindings {
         private ModifierKeyCode recipeKeybind = ModifierKeyCode.of(InputUtil.Type.KEYSYM.createFromCode(82), Modifier.none());
         private ModifierKeyCode usageKeybind = ModifierKeyCode.of(InputUtil.Type.KEYSYM.createFromCode(85), Modifier.none());
         private ModifierKeyCode hideKeybind = ModifierKeyCode.of(InputUtil.Type.KEYSYM.createFromCode(79), Modifier.of(false, true, false));
@@ -374,62 +367,94 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     
     public static class Appearance {
         @UseSpecialRecipeTypeScreen private RecipeScreenType recipeScreenType = RecipeScreenType.UNSET;
-        @Comment("Declares the appearance of REI windows.") private boolean darkTheme = false;
-        @Comment("The ordering of the items on the item panel.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
-        private ItemListOrderingConfig itemListOrdering = ItemListOrderingConfig.REGISTRY_ASCENDING;
-        @Comment("Declares the position of the search field.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
-        private SearchFieldLocation searchFieldLocation = SearchFieldLocation.CENTER;
-        @Comment("Declares the position of the item list panel.") private boolean mirrorItemPanel = false;
-        @Comment("Declares the maximum amount of recipes displayed in a page if possible.") @ConfigEntry.BoundedDiscrete(min = 2, max = 99)
-        private int maxRecipesPerPage = 15;
-        private boolean clickableRecipeArrows = true;
+        @Comment("Declares the appearance of REI windows.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
+        private AppearanceTheme theme = AppearanceTheme.LIGHT;
+        @ConfigEntry.Gui.CollapsibleObject
+        private Layout layout = new Layout();
         @Comment("Declares the appearance of recipe's border.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
         private RecipeBorderType recipeBorder = RecipeBorderType.DEFAULT;
-        @Comment("Declares whether REI should append mod names to item stacks.") private boolean appendModNames = true;
-        @Comment("Declares how the scrollbar in villager screen should act.") private boolean villagerScreenPermanentScrollBar = false;
-        @Comment("Declares whether entry list widget is scrolled.") private boolean scrollingEntryListWidget = false;
-        @Comment("Declares whether scrolled entry list widget should snap to rows.") private boolean snapToRows = false;
-        @Comment("Declares whether favorites tooltip should be displayed.") private boolean displayFavoritesTooltip = false;
-        @Comment("Declares whether favorites will be searched.") private boolean searchFavorites = true;
-        @UsePercentage(min = 0.25, max = 4.0) private double entrySize = 1.0;
-        private boolean useCompactTabs = true;
-        private boolean lowerConfigButton = true;
-        @Comment("Declares whether REI should resize its recipe window dynamically")
-        private boolean resizeDynamically = false;
-    }
-    
-    public static class Technical {
-        @Comment("To disable REI's default plugin.\nDon't change this unless you understand what you are doing!") private boolean loadDefaultPlugin = true;
-        @Comment("Declares the command used to change gamemode.") private String gamemodeCommand = "/gamemode {gamemode}";
-        @Comment("Declares the command used in servers to cheat items.") private String giveCommand = "/give {player_name} {item_identifier}{nbt} {count}";
-        @Comment("Declares the command used to change weather.") private String weatherCommand = "/weather {weather}";
-        private boolean registerRecipesInAnotherThread = true;
-        private boolean debugRenderTimeRequired = false;
-        @Comment("Experimental: Declares whether search time should be debugged.") private boolean debugSearchTimeRequired = false;
-    }
-    
-    public static class Modules {
-        @Comment("Declares whether the craftable filter button is enabled.") private boolean enableCraftableOnlyButton = false;
-        private boolean toastDisplayedOnCopyIdentifier = true;
-        @Comment("Declares whether the utils buttons are shown.") private boolean showUtilsButtons = false;
-        @Comment("Declares whether REI should remove the recipe book.") private boolean disableRecipeBook = false;
-        @Comment("Declares whether REI should fix closing container with tab.") private boolean fixTabCloseContainer = false;
-    }
-    
-    public static class Performance {
-        @Comment("Whether REI should render entry's enchantment glint") private boolean renderEntryEnchantmentGlint = true;
-        private boolean newFastEntryRendering = true;
-        @Comment("Experimental: Declares whether REI should search async.") private boolean asyncSearch = true;
-        @Comment("Experimental: Declares how many entries should be grouped one async search.") @ConfigEntry.BoundedDiscrete(min = 25, max = 400)
-        private int numberAsyncSearch = 50;
-    }
-    
-    public static class Filtering {
-        @UseFilteringScreen private List<EntryStack> filteredStacks = new ArrayList<>();
+        @Comment("Declares whether entry panel is scrolled.") private boolean scrollingEntryListWidget = false;
+        
+        public static class Layout {
+            @Comment("Declares the position of the search field.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
+            private SearchFieldLocation searchFieldLocation = SearchFieldLocation.CENTER;
+            @Comment("Declares the position of the config button.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
+            private ConfigButtonPosition configButtonLocation = ConfigButtonPosition.LOWER;
+            @Comment("Declares whether the craftable filter button is enabled.") private boolean enableCraftableOnlyButton = false;
+            @Comment("Declares whether the utils buttons are shown.") private boolean showUtilsButtons = false;
+        }
     }
     
-    @ApiStatus.Experimental
-    public static class Experimental {
-        private boolean isSubsetsEnabled = false;
+    public static class Functionality {
+        @Comment("Declares whether REI should remove the recipe book.") private boolean disableRecipeBook = false;
+        @Comment("Declares whether subsets is enabled.") private boolean isSubsetsEnabled = false;
+    }
+    
+    public static class Advanced {
+        @ConfigEntry.Gui.CollapsibleObject
+        private Tooltips tooltips = new Tooltips();
+        @ConfigEntry.Gui.CollapsibleObject
+        private Layout layout = new Layout();
+        @ConfigEntry.Gui.CollapsibleObject
+        private Accessibility accessibility = new Accessibility();
+        @ConfigEntry.Gui.CollapsibleObject
+        private Search search = new Search();
+        @ConfigEntry.Gui.CollapsibleObject
+        private Commands commands = new Commands();
+        @ConfigEntry.Gui.CollapsibleObject
+        private Miscellaneous miscellaneous = new Miscellaneous();
+        @ConfigEntry.Gui.CollapsibleObject(startExpanded = true)
+        private Filtering filtering = new Filtering();
+        
+        public static class Tooltips {
+            @Comment("Declares whether REI should append mod names to entries.") private boolean appendModNames = true;
+            @Comment("Declares whether favorites tooltip should be displayed.") private boolean displayFavoritesTooltip = false;
+        }
+        
+        public static class Layout {
+            @Comment("The ordering of the items on the entry panel.")
+            @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
+            private EntryPanelOrderingConfig entryPanelOrdering = EntryPanelOrderingConfig.REGISTRY_ASCENDING;
+            @Comment("Declares the maximum amount of recipes displayed in a page if possible.") @ConfigEntry.BoundedDiscrete(min = 2, max = 99)
+            private int maxRecipesPerPage = 15;
+            @Comment("Declares whether entry rendering time should be debugged.") private boolean debugRenderTimeRequired = false;
+        }
+        
+        public static class Accessibility {
+            @UsePercentage(min = 0.25, max = 4.0) private double entrySize = 1.0;
+            @Comment("Declares the position of the entry panel.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
+            private DisplayPanelLocation displayPanelLocation = DisplayPanelLocation.RIGHT;
+            @Comment("Declares whether scrolled entry panel should snap to rows.") private boolean snapToRows = false;
+            @Comment("Declares how the scrollbar in villager screen should act.") private boolean villagerScreenPermanentScrollBar = false;
+            private boolean toastDisplayedOnCopyIdentifier = true;
+            @Comment("Declares whether REI should use compact tabs for categories.") private boolean useCompactTabs = true;
+            @Comment("Declares whether REI should resize its recipe window dynamically") private boolean resizeDynamically = false;
+        }
+        
+        public static class Search {
+            @Comment("Declares whether favorites will be searched.") private boolean searchFavorites = true;
+            @Comment("Declares whether search time should be debugged.") private boolean debugSearchTimeRequired = false;
+            @Comment("Declares whether REI should search async.") private boolean asyncSearch = true;
+            @Comment("Declares how many entries should be grouped one async search.") @ConfigEntry.BoundedDiscrete(min = 25, max = 400)
+            private int numberAsyncSearch = 50;
+        }
+        
+        public static class Commands {
+            @Comment("Declares the command used to change gamemode.") private String gamemodeCommand = "/gamemode {gamemode}";
+            @Comment("Declares the command used in servers to cheat items.") private String giveCommand = "/give {player_name} {item_identifier}{nbt} {count}";
+            @Comment("Declares the command used to change weather.") private String weatherCommand = "/weather {weather}";
+        }
+        
+        public static class Miscellaneous {
+            @Comment("Declares whether arrows in containers should be clickable.") private boolean clickableRecipeArrows = true;
+            @Comment("To disable REI's default plugin.\nDon't change this unless you understand what you are doing!") private boolean loadDefaultPlugin = true;
+            private boolean registerRecipesInAnotherThread = true;
+            @Comment("Whether REI should render entry's enchantment glint") private boolean renderEntryEnchantmentGlint = true;
+            private boolean newFastEntryRendering = true;
+        }
+        
+        public static class Filtering {
+            @UseFilteringScreen private List<EntryStack> filteredStacks = new ArrayList<>();
+        }
     }
 }

+ 1 - 1
src/main/java/me/shedaniel/rei/impl/ScreenHelper.java

@@ -26,7 +26,7 @@ package me.shedaniel.rei.impl;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import me.shedaniel.cloth.hooks.ClothClientHooks;
+import me.shedaniel.cloth.api.client.events.v0.ClothClientHooks;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.api.Executor;
 import me.shedaniel.rei.RoughlyEnoughItemsState;

+ 5 - 5
src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationCategory.java

@@ -39,7 +39,6 @@ import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.impl.RenderingEntry;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.class_5348;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.render.BufferBuilder;
@@ -48,6 +47,7 @@ import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.StringRenderable;
 import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.Matrix4f;
@@ -134,7 +134,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
     
     private static class ScrollableTextWidget extends WidgetWithBounds {
         private Rectangle bounds;
-        private List<class_5348> texts;
+        private List<StringRenderable> texts;
         private final ScrollingContainer scrolling = new ScrollingContainer() {
             @Override
             public Rectangle getBounds() {
@@ -145,7 +145,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
             @Override
             public int getMaxScrollHeight() {
                 int i = 2;
-                for (class_5348 entry : texts) {
+                for (StringRenderable entry : texts) {
                     i += entry == null ? 4 : font.fontHeight;
                 }
                 return i;
@@ -155,7 +155,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
         public ScrollableTextWidget(Rectangle bounds, List<Text> texts) {
             this.bounds = Objects.requireNonNull(bounds);
             this.texts = Lists.newArrayList();
-            for (class_5348 text : texts) {
+            for (StringRenderable text : texts) {
                 if (!this.texts.isEmpty())
                     this.texts.add(null);
                 this.texts.addAll(MinecraftClient.getInstance().textRenderer.wrapStringToWidthAsList(text, bounds.width - 11));
@@ -197,7 +197,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
             Rectangle innerBounds = scrolling.getScissorBounds();
             ScissorsHandler.INSTANCE.scissor(innerBounds);
             int currentY = (int) -scrolling.scrollAmount + innerBounds.y;
-            for (class_5348 text : texts) {
+            for (StringRenderable text : texts) {
                 if (text != null && currentY + font.fontHeight >= innerBounds.y && currentY <= innerBounds.getMaxY()) {
                     font.draw(matrices, text, innerBounds.x + 2, currentY + 2, REIHelper.getInstance().isDarkThemeEnabled() ? 0xFFBBBBBB : 0xFF090909);
                 }

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/bg_bg.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Експериментални функции",
   "config.roughlyenoughitems.cheating": "Измама:",
   "config.roughlyenoughitems.smooth_scrolling": "Smooth Scrolling Settings",
-  "config.roughlyenoughitems.recipeKeybind": "Показване на рецепта:",
-  "config.roughlyenoughitems.hideKeybind": "Hide/Show REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Показване на рецепта:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Hide/Show REI:",
   "config.roughlyenoughitems.usageKeybind": "Показване на употребата:",
   "config.roughlyenoughitems.nextPageKeybind": "Следваща страница:",
   "config.roughlyenoughitems.previousPageKeybind": "Предишна страница:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/de_de.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Experimentelle Funktionen",
   "config.roughlyenoughitems.cheating": "Schummeln:",
   "config.roughlyenoughitems.smooth_scrolling": "Sanftes Scrollen Einstellungen",
-  "config.roughlyenoughitems.recipeKeybind": "Zeige Rezept:",
-  "config.roughlyenoughitems.hideKeybind": "Verstecke/Zeige REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Zeige Rezept:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Verstecke/Zeige REI:",
   "config.roughlyenoughitems.usageKeybind": "Zeige Verwendungen:",
   "config.roughlyenoughitems.nextPageKeybind": "Nächste Seite:",
   "config.roughlyenoughitems.previousPageKeybind": "Vorherige Seite:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/en_ud.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Experimental Features",
   "config.roughlyenoughitems.cheating": ":ƃuᴉʇɐǝɥƆ",
   "config.roughlyenoughitems.smooth_scrolling": "sƃuᴉʇʇǝS ƃuᴉlloɹɔS ɥʇooɯS",
-  "config.roughlyenoughitems.recipeKeybind": "ǝdıɔǝᴚ ʍoɥS:",
-  "config.roughlyenoughitems.hideKeybind": "Hide/Show REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "ǝdıɔǝᴚ ʍoɥS:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Hide/Show REI:",
   "config.roughlyenoughitems.usageKeybind": "Show Uses:",
   "config.roughlyenoughitems.nextPageKeybind": "Next Page:",
   "config.roughlyenoughitems.previousPageKeybind": "Previous Page:",

+ 71 - 75
src/main/resources/assets/roughlyenoughitems/lang/en_us.json

@@ -101,104 +101,100 @@
   "subsets.rei.roughlyenoughitems.item_groups": "Creative Tabs",
   "_comment": "Config Tooltips",
   "config.roughlyenoughitems.title": "Roughly Enough Items Config",
-  "config.roughlyenoughitems.!general": "General",
+  "config.roughlyenoughitems.basics": "Basics",
   "config.roughlyenoughitems.appearance": "Appearance",
-  "config.roughlyenoughitems.modules": "Modules",
-  "config.roughlyenoughitems.technical": "Technical",
-  "config.roughlyenoughitems.performance": "Performance",
-  "config.roughlyenoughitems.filtering": "Filtering",
-  "config.roughlyenoughitems.experimental": "Experimental Features",
+  "config.roughlyenoughitems.functionality": "Functionality",
+  "config.roughlyenoughitems.advanced": "Advanced",
   "config.roughlyenoughitems.cheating": "Cheating:",
-  "config.roughlyenoughitems.smooth_scrolling": "Smooth Scrolling Settings",
-  "config.roughlyenoughitems.recipeKeybind": "Show Recipe:",
-  "config.roughlyenoughitems.hideKeybind": "Hide/Show REI:",
-  "config.roughlyenoughitems.usageKeybind": "Show Uses:",
-  "config.roughlyenoughitems.nextPageKeybind": "Next Page:",
-  "config.roughlyenoughitems.previousPageKeybind": "Previous Page:",
-  "config.roughlyenoughitems.focusSearchFieldKeybind": "Focus Search Field:",
-  "config.roughlyenoughitems.copyRecipeIdentifierKeybind": "Copy Recipe Identifier:",
-  "config.roughlyenoughitems.exportImageKeybind": "Export Recipe:",
-  "config.roughlyenoughitems.favoriteKeybind": "Favorite Entry:",
   "config.roughlyenoughitems.favoritesEnabled": "Favorites Enabled:",
-  "config.roughlyenoughitems.clickableRecipeArrows": "Clickable Recipe Arrows:",
+  "config.roughlyenoughitems.keyBindings": "Keybindings",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Show Recipe:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Hide/Show REI:",
+  "config.roughlyenoughitems.keyBindings.usageKeybind": "Show Uses:",
+  "config.roughlyenoughitems.keyBindings.nextPageKeybind": "Next Page:",
+  "config.roughlyenoughitems.keyBindings.previousPageKeybind": "Previous Page:",
+  "config.roughlyenoughitems.keyBindings.focusSearchFieldKeybind": "Focus Search Field:",
+  "config.roughlyenoughitems.keyBindings.copyRecipeIdentifierKeybind": "Copy Recipe Identifier:",
+  "config.roughlyenoughitems.keyBindings.exportImageKeybind": "Export Recipe:",
+  "config.roughlyenoughitems.keyBindings.favoriteKeybind": "Favorite Entry:",
+  "config.roughlyenoughitems.recipeScreenType": "Recipe Screen Type:",
+  "config.roughlyenoughitems.recipeScreenType.config": "Recipe Screen Type: %s",
+  "config.roughlyenoughitems.recipeScreenType.unset": "Not Set",
+  "config.roughlyenoughitems.recipeScreenType.original": "Original",
+  "config.roughlyenoughitems.recipeScreenType.villager": "Villager Like",
+  "config.roughlyenoughitems.layout": "Layout",
+  "config.roughlyenoughitems.tooltips": "Tooltips",
+  "config.roughlyenoughitems.accessibility": "Accessibility",
+  "config.roughlyenoughitems.search": "Search",
+  "config.roughlyenoughitems.commands": "Commands",
+  "config.roughlyenoughitems.filtering": "Filtering",
+  "config.roughlyenoughitems.miscellaneous": "Miscellaneous",
+  "config.roughlyenoughitems.miscellaneous.clickableRecipeArrows": "Clickable Recipe Arrows:",
   "config.roughlyenoughitems.isSubsetsEnabled": "Subsets Enabled:",
-  "config.roughlyenoughitems.clickableRecipeArrows.boolean.true": "Enabled",
-  "config.roughlyenoughitems.clickableRecipeArrows.boolean.false": "Disabled",
-  "config.roughlyenoughitems.renderEntryEnchantmentGlint": "Render Enchantment Glint:",
-  "config.roughlyenoughitems.lowerConfigButton": "Config Button Position:",
-  "config.roughlyenoughitems.lowerConfigButton.boolean.false": "Top",
-  "config.roughlyenoughitems.lowerConfigButton.boolean.true": "Next to Search",
+  "config.roughlyenoughitems.miscellaneous.renderEntryEnchantmentGlint": "Render Enchantment Glint:",
+  "config.roughlyenoughitems.layout.configButtonLocation": "Config Button Position:",
+  "config.roughlyenoughitems.layout.configButtonLocation.upper": "Upper",
+  "config.roughlyenoughitems.layout.configButtonLocation.lower": "Lower",
   "config.roughlyenoughitems.filteredEntries.selectAll": "Select All",
   "config.roughlyenoughitems.filteredEntries.selectNone": "Unselect All",
   "config.roughlyenoughitems.filteredEntries.hide": "Hide Selected",
   "config.roughlyenoughitems.filteredEntries.show": "Show Selected",
   "config.roughlyenoughitems.filteredEntries.loadWorldFirst": "Load World First!",
-  "config.roughlyenoughitems.entrySize": "Entry Size:",
-  "config.roughlyenoughitems.asyncSearch": "Async Search:",
-  "config.roughlyenoughitems.numberAsyncSearch": "Async Entry Group Size:",
-  "config.roughlyenoughitems.useCompactTabs": "Compact Tabs:",
-  "config.roughlyenoughitems.darkTheme": "Appearance Theme:",
-  "config.roughlyenoughitems.darkTheme.boolean.true": "Dark Theme",
-  "config.roughlyenoughitems.darkTheme.boolean.false": "Light Theme",
+  "config.roughlyenoughitems.accessibility.entrySize": "Entry Size:",
+  "config.roughlyenoughitems.search.asyncSearch": "Async Search:",
+  "config.roughlyenoughitems.search.numberAsyncSearch": "Async Group Size:",
+  "config.roughlyenoughitems.accessibility.useCompactTabs": "Compact Tabs:",
+  "config.roughlyenoughitems.theme": "Appearance Theme:",
+  "config.roughlyenoughitems.theme.dark": "Dark Theme",
+  "config.roughlyenoughitems.theme.light": "Light Theme",
   "config.roughlyenoughitems.recipeBorder": "Recipe Display Border:",
   "config.roughlyenoughitems.recipeBorder.lighter": "Lighter",
   "config.roughlyenoughitems.recipeBorder.default": "Default",
   "config.roughlyenoughitems.recipeBorder.none": "None",
-  "config.roughlyenoughitems.maxRecipesPerPage": "Maximum Recipes Per Page:",
-  "config.roughlyenoughitems.mirrorItemPanel": "Item List Position:",
-  "config.roughlyenoughitems.mirrorItemPanel.boolean.true": "Left Side",
-  "config.roughlyenoughitems.mirrorItemPanel.boolean.false": "Right Side",
-  "config.roughlyenoughitems.debugRenderTimeRequired": "Entry List Debug Mode:",
-  "config.roughlyenoughitems.debugSearchTimeRequired": "Search Debug Mode:",
-  "config.roughlyenoughitems.resizeDynamically": "Resize Dynamically:",
-  "config.roughlyenoughitems.searchFieldLocation": "Search Field Position:",
-  "config.roughlyenoughitems.searchFieldLocation.bottom_side": "Bottom Left / Right",
-  "config.roughlyenoughitems.searchFieldLocation.top_side": "Top Left / Right",
-  "config.roughlyenoughitems.searchFieldLocation.center": "Middle",
-  "config.roughlyenoughitems.villagerScreenPermanentScrollBar": "Scrollbar Fade:",
-  "config.roughlyenoughitems.villagerScreenPermanentScrollBar.boolean.true": "Never",
-  "config.roughlyenoughitems.villagerScreenPermanentScrollBar.boolean.false": "When Inactive",
+  "config.roughlyenoughitems.layout.maxRecipesPerPage": "Maximum Recipes Per Page:",
+  "config.roughlyenoughitems.accessibility.displayPanelLocation": "Entry Panel Position:",
+  "config.roughlyenoughitems.accessibility.displayPanelLocation.left": "Left Side",
+  "config.roughlyenoughitems.accessibility.displayPanelLocation.right": "Right Side",
+  "config.roughlyenoughitems.layout.debugRenderTimeRequired": "Entry Panel Debug Mode:",
+  "config.roughlyenoughitems.search.debugSearchTimeRequired": "Search Debug Mode:",
+  "config.roughlyenoughitems.accessibility.resizeDynamically": "Resize Dynamically:",
+  "config.roughlyenoughitems.layout.searchFieldLocation": "Search Field Position:",
+  "config.roughlyenoughitems.layout.searchFieldLocation.bottom_side": "Bottom Left / Right",
+  "config.roughlyenoughitems.layout.searchFieldLocation.top_side": "Top Left / Right",
+  "config.roughlyenoughitems.layout.searchFieldLocation.center": "Middle",
+  "config.roughlyenoughitems.accessibility.villagerScreenPermanentScrollBar": "Scrollbar Fade:",
+  "config.roughlyenoughitems.accessibility.villagerScreenPermanentScrollBar.boolean.true": "Never",
+  "config.roughlyenoughitems.accessibility.villagerScreenPermanentScrollBar.boolean.false": "When Inactive",
   "config.roughlyenoughitems.disableRecipeBook": "Vanilla Recipe Book:",
-  "config.roughlyenoughitems.disableRecipeBook.boolean.true": "Disabled",
-  "config.roughlyenoughitems.disableRecipeBook.boolean.false": "Enabled",
+  "config.roughlyenoughitems.disableRecipeBook.boolean.true": "§cNo",
+  "config.roughlyenoughitems.disableRecipeBook.boolean.false": "§aYes",
   "config.roughlyenoughitems.fixTabCloseContainer": "Fix Vanilla Tab Container (When Recipe Book Disabled):",
   "config.roughlyenoughitems.lighterButtonHover": "Lighter Button Hover:",
-  "config.roughlyenoughitems.enableCraftableOnlyButton": "Craftable Filter:",
-  "config.roughlyenoughitems.enableCraftableOnlyButton.boolean.true": "Enabled",
-  "config.roughlyenoughitems.enableCraftableOnlyButton.boolean.false": "Disabled",
-  "config.roughlyenoughitems.showUtilsButtons": "Utils Buttons:",
-  "config.roughlyenoughitems.showUtilsButtons.boolean.true": "Enabled",
-  "config.roughlyenoughitems.showUtilsButtons.boolean.false": "Disabled",
-  "config.roughlyenoughitems.gamemodeCommand": "Game Mode Command:",
-  "config.roughlyenoughitems.giveCommand": "Give Command:",
-  "config.roughlyenoughitems.loadDefaultPlugin": "Load Default Plugin:",
-  "config.roughlyenoughitems.loadDefaultPlugin.boolean.false": "§cNo (Dangerous)",
-  "config.roughlyenoughitems.registerRecipesInAnotherThread": "Recipe Sync Thread:",
-  "config.roughlyenoughitems.registerRecipesInAnotherThread.boolean.true": "New REI Thread",
-  "config.roughlyenoughitems.registerRecipesInAnotherThread.boolean.false": "§cPacket Thread",
-  "config.roughlyenoughitems.weatherCommand": "Weather Command:",
-  "config.roughlyenoughitems.itemListOrdering": "Entry List Ordering:",
+  "config.roughlyenoughitems.layout.enableCraftableOnlyButton": "Craftable Filter:",
+  "config.roughlyenoughitems.layout.showUtilsButtons": "Utils Buttons:",
+  "config.roughlyenoughitems.commands.gamemodeCommand": "Game Mode Command:",
+  "config.roughlyenoughitems.commands.giveCommand": "Give Command:",
+  "config.roughlyenoughitems.miscellaneous.loadDefaultPlugin": "Load Default Plugin:",
+  "config.roughlyenoughitems.miscellaneous.loadDefaultPlugin.boolean.false": "§cNo (Dangerous)",
+  "config.roughlyenoughitems.miscellaneous.registerRecipesInAnotherThread": "REI Reload Thread:",
+  "config.roughlyenoughitems.miscellaneous.registerRecipesInAnotherThread.boolean.true": "REI Thread",
+  "config.roughlyenoughitems.miscellaneous.registerRecipesInAnotherThread.boolean.false": "§cPacket Thread",
+  "config.roughlyenoughitems.commands.weatherCommand": "Weather Command:",
+  "config.roughlyenoughitems.layout.entryPanelOrdering": "Entry Panel Ordering:",
   "config.roughlyenoughitems.list_ordering_button": "%s [%s]",
-  "config.roughlyenoughitems.newFastEntryRendering": "Faster Entry Rendering:",
-  "config.roughlyenoughitems.recipeScreenType": "Recipe Screen Type:",
-  "config.roughlyenoughitems.recipeScreenType.config": "Recipe Screen Type: %s",
-  "config.roughlyenoughitems.recipeScreenType.unset": "Not Set",
-  "config.roughlyenoughitems.recipeScreenType.original": "Original",
-  "config.roughlyenoughitems.recipeScreenType.villager": "Villager Like",
+  "config.roughlyenoughitems.miscellaneous.newFastEntryRendering": "Faster Entry Rendering:",
   "config.roughlyenoughitems.itemCheatingMode": "Item Cheating Amount:",
   "config.roughlyenoughitems.itemCheatingMode.rei_like": "Normal",
   "config.roughlyenoughitems.itemCheatingMode.jei_like": "Inverted",
-  "config.roughlyenoughitems.searchFavorites": "Search Filter in Favorites:",
-  "config.roughlyenoughitems.appendModNames": "Append Mod Names:",
-  "config.roughlyenoughitems.displayFavoritesOnTheLeft": "Favorites Position:",
-  "config.roughlyenoughitems.displayFavoritesOnTheLeft.boolean.true": "Opposite Side",
-  "config.roughlyenoughitems.displayFavoritesOnTheLeft.boolean.false": "Top of list",
-  "config.roughlyenoughitems.displayFavoritesTooltip": "Display Favorites Tooltips:",
-  "config.roughlyenoughitems.snapToRows": "Entry List Snap To Rows:",
-  "config.roughlyenoughitems.toastDisplayedOnCopyIdentifier": "Copy Identifier Toast:",
+  "config.roughlyenoughitems.search.searchFavorites": "Search Filter in Favorites:",
+  "config.roughlyenoughitems.tooltips.appendModNames": "Append Mod Names:",
+  "config.roughlyenoughitems.tooltips.displayFavoritesTooltip": "Append Favorites Hint:",
+  "config.roughlyenoughitems.accessibility.snapToRows": "Entry Panel Snap Rows:",
+  "config.roughlyenoughitems.accessibility.toastDisplayedOnCopyIdentifier": "Copy Identifier Toast:",
   "config.roughlyenoughitems.scrollingEntryListWidget": "Entry List Action:",
   "config.roughlyenoughitems.scrollingEntryListWidget.boolean.true": "Scrolled",
   "config.roughlyenoughitems.scrollingEntryListWidget.boolean.false": "Paginated",
+  "config.roughlyenoughitems.filteringScreen": "Customized Filtering",
   "language.roughlyenoughitems.english": "English",
   "language.roughlyenoughitems.japanese": "Japanese",
   "language.roughlyenoughitems.chinese_simplified": "Chinese Simplified",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/et_ee.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Eksperimentaalsed funktsioonid",
   "config.roughlyenoughitems.cheating": "Sohitegemine:",
   "config.roughlyenoughitems.smooth_scrolling": "Sujuva kerimise sätted",
-  "config.roughlyenoughitems.recipeKeybind": "Näita retsepti:",
-  "config.roughlyenoughitems.hideKeybind": "Peida/kuva REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Näita retsepti:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Peida/kuva REI:",
   "config.roughlyenoughitems.usageKeybind": "Näita kasutusvõimalusi:",
   "config.roughlyenoughitems.nextPageKeybind": "Järgmine leht:",
   "config.roughlyenoughitems.previousPageKeybind": "Eelmine leht:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/fr_fr.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Fonctionnalités expérimentales",
   "config.roughlyenoughitems.cheating": "Triche :",
   "config.roughlyenoughitems.smooth_scrolling": "Paramètres de défilement régulier",
-  "config.roughlyenoughitems.recipeKeybind": "Afficher la recette :",
-  "config.roughlyenoughitems.hideKeybind": "Masquer/afficher REI :",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Afficher la recette :",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Masquer/afficher REI :",
   "config.roughlyenoughitems.usageKeybind": "Afficher les utilisations :",
   "config.roughlyenoughitems.nextPageKeybind": "Page suivante :",
   "config.roughlyenoughitems.previousPageKeybind": "Page précédente :",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/ja_jp.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Experimental Features",
   "config.roughlyenoughitems.cheating": "チートモード:",
   "config.roughlyenoughitems.smooth_scrolling": "Smooth Scrolling Settings",
-  "config.roughlyenoughitems.recipeKeybind": "Show Recipe:",
-  "config.roughlyenoughitems.hideKeybind": "REIを表示/非表示:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Show Recipe:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "REIを表示/非表示:",
   "config.roughlyenoughitems.usageKeybind": "Show Uses:",
   "config.roughlyenoughitems.nextPageKeybind": "次のページ:",
   "config.roughlyenoughitems.previousPageKeybind": "前のページ:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/lol_us.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Experimental Features",
   "config.roughlyenoughitems.cheating": "Cheating:",
   "config.roughlyenoughitems.smooth_scrolling": "Smooth Scrolling Settings",
-  "config.roughlyenoughitems.recipeKeybind": "Show Recipe:",
-  "config.roughlyenoughitems.hideKeybind": "Hide/Show REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Show Recipe:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Hide/Show REI:",
   "config.roughlyenoughitems.usageKeybind": "Show Uses:",
   "config.roughlyenoughitems.nextPageKeybind": "Next Page:",
   "config.roughlyenoughitems.previousPageKeybind": "Previous Page:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/nn_no.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Experimental Features",
   "config.roughlyenoughitems.cheating": "Cheating:",
   "config.roughlyenoughitems.smooth_scrolling": "Smooth Scrolling Settings",
-  "config.roughlyenoughitems.recipeKeybind": "Show Recipe:",
-  "config.roughlyenoughitems.hideKeybind": "Hide/Show REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Show Recipe:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Hide/Show REI:",
   "config.roughlyenoughitems.usageKeybind": "Show Uses:",
   "config.roughlyenoughitems.nextPageKeybind": "Next Page:",
   "config.roughlyenoughitems.previousPageKeybind": "Previous Page:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/pl_pl.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Experimental Features",
   "config.roughlyenoughitems.cheating": "Tryb Oszukiwania:",
   "config.roughlyenoughitems.smooth_scrolling": "Ustawienia gładkiego przewijania",
-  "config.roughlyenoughitems.recipeKeybind": "Pokaż recepturę:",
-  "config.roughlyenoughitems.hideKeybind": "Ukryj/Pokaż REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Pokaż recepturę:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Ukryj/Pokaż REI:",
   "config.roughlyenoughitems.usageKeybind": "Pokaż użycia przedmiotu:",
   "config.roughlyenoughitems.nextPageKeybind": "Następna strona:",
   "config.roughlyenoughitems.previousPageKeybind": "Poprzednia strona:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/pt_br.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Função Experimental",
   "config.roughlyenoughitems.cheating": "Trapaças:",
   "config.roughlyenoughitems.smooth_scrolling": "Configurações de rolagem suave",
-  "config.roughlyenoughitems.recipeKeybind": "Mostrar Receita:",
-  "config.roughlyenoughitems.hideKeybind": "Esconder/Mostrar REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Mostrar Receita:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Esconder/Mostrar REI:",
   "config.roughlyenoughitems.usageKeybind": "Mostrar Usos:",
   "config.roughlyenoughitems.nextPageKeybind": "Próxima página:",
   "config.roughlyenoughitems.previousPageKeybind": "Página Anterior:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/pt_pt.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Experimental Features",
   "config.roughlyenoughitems.cheating": "Batotas:",
   "config.roughlyenoughitems.smooth_scrolling": "Configurações de Rolagem Suave",
-  "config.roughlyenoughitems.recipeKeybind": "Mostrar Receita:",
-  "config.roughlyenoughitems.hideKeybind": "Esconder/Mostrar REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Mostrar Receita:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Esconder/Mostrar REI:",
   "config.roughlyenoughitems.usageKeybind": "Mostrar Usos:",
   "config.roughlyenoughitems.nextPageKeybind": "Página Posterior:",
   "config.roughlyenoughitems.previousPageKeybind": "Página Anterior:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/ru_ru.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "Экспериментальные возможности",
   "config.roughlyenoughitems.cheating": "Мошенничество:",
   "config.roughlyenoughitems.smooth_scrolling": "Настройки плавной прокрутки",
-  "config.roughlyenoughitems.recipeKeybind": "Показать рецепт:",
-  "config.roughlyenoughitems.hideKeybind": "Скрыть/Показать REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "Показать рецепт:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "Скрыть/Показать REI:",
   "config.roughlyenoughitems.usageKeybind": "Показать использование:",
   "config.roughlyenoughitems.nextPageKeybind": "Следующая страница:",
   "config.roughlyenoughitems.previousPageKeybind": "Предыдущая страница:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/zh_cn.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "实验性功能",
   "config.roughlyenoughitems.cheating": "作弊:",
   "config.roughlyenoughitems.smooth_scrolling": "平滑滚动设定",
-  "config.roughlyenoughitems.recipeKeybind": "显示食谱:",
-  "config.roughlyenoughitems.hideKeybind": "隐藏/显示 REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "显示食谱:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "隐藏/显示 REI:",
   "config.roughlyenoughitems.usageKeybind": "显示用途:",
   "config.roughlyenoughitems.nextPageKeybind": "下一页:",
   "config.roughlyenoughitems.previousPageKeybind": "上一页:",

+ 2 - 2
src/main/resources/assets/roughlyenoughitems/lang/zh_tw.json

@@ -110,8 +110,8 @@
   "config.roughlyenoughitems.experimental": "實驗性功能",
   "config.roughlyenoughitems.cheating": "作弊:",
   "config.roughlyenoughitems.smooth_scrolling": "平滑滾動設定",
-  "config.roughlyenoughitems.recipeKeybind": "顯示合成配方:",
-  "config.roughlyenoughitems.hideKeybind": "隱藏/顯示 REI:",
+  "config.roughlyenoughitems.keyBindings.recipeKeybind": "顯示合成配方:",
+  "config.roughlyenoughitems.keyBindings.hideKeybind": "隱藏/顯示 REI:",
   "config.roughlyenoughitems.usageKeybind": "顯示用途配方:",
   "config.roughlyenoughitems.nextPageKeybind": "下一頁:",
   "config.roughlyenoughitems.previousPageKeybind": "上一頁:",

+ 2 - 2
src/main/resources/fabric.mod.json

@@ -36,9 +36,9 @@
   },
   "depends": {
     "fabricloader": "*",
-    "cloth": ">=2-",
+    "cloth-client-events-v0": ">=1.0.2",
     "cloth-basic-math": "*",
-    "cloth-config2": ">=3-",
+    "cloth-config2": ">=4-",
     "minecraft": "~1.16-Snapshot.20.16.a"
   },
   "mixins": [